Управление памятью C для кроссплатформенной виртуальной машины

Я задал вопрос о размерах C-типа, которые я получаю довольно хороший ответ, но я понял, что могу не очень хорошо сформулировать вопрос, чтобы быть полезным для моей цели.

Я работал инженером-программистом до того, как перешел на инженера-программиста, поэтому мне нравятся компьютерные архитектуры, и я всегда думаю о создании ВМ. Я только что закончил интересный проект по созданию виртуальной машины на Java, которым очень горжусь. Но есть некоторые юридические проблемы. Я не могу открыть его сейчас, и у меня сейчас есть свободное время. Поэтому я хочу посмотреть, смогу ли я создать еще одну виртуальную машину на C (с большей скоростью) просто для развлечения и обучения.

Дело в том, что я не являюсь программой на языке C, когда в последний раз я писал нетривиальную проблему на языке C более 10 лет назад. Я был программистом на Паскале, Delphi, а теперь на Java и PHP.

Есть ряд препятствий, которые я могу предвидеть, и я пытаюсь решить одно, а именно доступ к существующей библиотеке (в Java отражение решает эту проблему).

Я планирую решить эту проблему с помощью буфера данных (похожего на стек). Клиент моей виртуальной машины может запрограммировать помещать данные в этот стек перед тем, как передать мне указатель на собственную функцию.

int main(void) {
    // Prepare stack
    int   aStackSize = 1024*4;
    char *aStackData = malloc(aStackSize);

    // Initialise stack
    VMStack aStack;
    VMStack_Initialize(&aStack, (char *)aStackData, aStackSize);

    // Push in the parameters
    char *Params = VMStack_CurrentPointer(&aStack);
    VMStack_Push_int   (&aStack, 10  ); // Push an int
    VMStack_Push_double(&aStack, 15.3); // Push a double

    // Prepare space for the expected return
    char *Result = VMStack_CurrentPointer(&aStack);
    VMStack_Push_double(&aStack, 0.0); // Push an empty double for result

    // Execute
    void (*NativeFunction)(char*, char*) = &Plus;
    NativeFunction(Params, Result); // Call the function

    // Show the result
    double ResultValue = VMStack_Pull_double(&aStack); // Get the result
    printf("Result:  %5.2f\n", ResultValue);               // Print the result

    // Remove the previous parameters
    VMStack_Pull_double(&aStack); // Pull to clear space of the parameter
    VMStack_Pull_int   (&aStack); // Pull to clear space of the parameter

    // Just to be sure, print out the pointer and see if it is `0`
    printf("Pointer: %d\n", aStack.Pointer);

    free(aStackData);
    return EXIT_SUCCESS;
}

Выталкивание, вытягивание и вызов собственной функции может быть инициировано байтовым кодом (именно так позже будет создана виртуальная машина).

Для полноты (чтобы вы могли опробовать его на своем компьютере) вот код для Stack:

typedef struct {
    int  Pointer;
    int  Size;
    char *Data;
} VMStack;

inline void   VMStack_Initialize(VMStack *pStack, char *pData, int pSize) __attribute__((always_inline));
inline char   *VMStack_CurrentPointer(VMStack *pStack)                    __attribute__((always_inline));
inline void   VMStack_Push_int(VMStack *pStack, int pData)                __attribute__((always_inline));
inline void   VMStack_Push_double(VMStack *pStack, double pData)          __attribute__((always_inline));
inline int    VMStack_Pull_int(VMStack *pStack)                           __attribute__((always_inline));
inline double VMStack_Pull_double(VMStack *pStack)                        __attribute__((always_inline));

inline void VMStack_Initialize(VMStack *pStack, char *pData, int pSize) {
    pStack->Pointer = 0;
    pStack->Data    = pData;
    pStack->Size    = pSize;
}

inline char *VMStack_CurrentPointer(VMStack *pStack) {
    return (char *)(pStack->Pointer + pStack->Data);
}

inline void VMStack_Push_int(VMStack *pStack, int pData) {
    *(int *)(pStack->Data + pStack->Pointer) = pData;
    pStack->Pointer += sizeof pData; // Should check the overflow
}
inline void VMStack_Push_double(VMStack *pStack, double pData) {
    *(double *)(pStack->Data + pStack->Pointer) = pData;
    pStack->Pointer += sizeof pData; // Should check the overflow
}

inline int VMStack_Pull_int(VMStack *pStack) {
    pStack->Pointer -= sizeof(int);// Should check the underflow
    return *((int *)(pStack->Data + pStack->Pointer));
}
inline double VMStack_Pull_double(VMStack *pStack) {
    pStack->Pointer -= sizeof(double);// Should check the underflow
    return *((double *)(pStack->Data + pStack->Pointer));
}

На стороне собственных функций я создал следующее для тестирования:

// These two structures are there so that Plus will not need to access its parameter using
//    arithmetic-pointer operation (to reduce mistake and hopefully for better speed).
typedef struct {
    int    A;
    double B;
} Data;
typedef struct {
    double D;
} DDouble;

// Here is a helper function for displaying void PrintData(Data *pData, DDouble *pResult) { printf("%5.2f + %5.2f = %5.2f\n", pData->A*1.0, pData->B, pResult->D); }

// Some native function void Plus(char* pParams, char* pResult) { Data *D = (Data *)pParams; // Access data without arithmetic-pointer operation DDouble *DD = (DDouble *)pResult; // Same for return DD->D = D->A + D->B; PrintData(D, DD); }

При выполнении приведенный выше код возвращает:

10.00 + 15.30 = 25.30
Result:  25.30
Pointer: 0

Это хорошо работает на моей машине (Linux x86 32 бит GCC-C99). Было бы очень хорошо, если бы это работало и на другой ОС / архитектуре. Но мы должны знать, ПО МЕНЮ, три проблемы, связанные с памятью.

1). Размер данных. Похоже, что если я компилирую как виртуальную машину, так и собственные функции, используя один и тот же компилятор на одной архитектуре, типы размеров должны быть одинаковыми.

2). Порядок байтов - то же самое с размером данных.

3). Выравнивание памяти - это проблема, поскольку байты заполнения могут быть добавлены в структуру, но их трудно синхронизировать при подготовке стека параметров как (нет способа узнать, как добавляется заполнение, за исключением жесткого кодирования).

Мои вопросы:

1). Если я знаю размер типов, есть ли способ изменить функцию push и pull для точной синхронизации с заполнением структуры? (измените, чтобы компилятор позаботился об этом, как о задачах Datasize и Endians).

2). Если я упаковываю структуру по одному (используя #pragma pack(1)); (2.1) Будет ли приемлемо снижение производительности? и (2.2) Будет ли под угрозой стабильность программы?

3). Как насчет заполнения 2,4 или 8? Что должно быть хорошо для общей 32- или 64-битной системы?

4). Можете ли вы дать мне документацию по точному алгоритму заполнения, например, для GCC на x86?

5). Есть способ лучше?

ПРИМЕЧАНИЕ: кроссплатформенность - это не моя конечная цель, но я не могу устоять. Кроме того, производительность не является моей целью, раз уж она не такая уж уродливая. Все это для развлечения и обучения.

Извините за мой английский и очень длинный пост.

Заранее всем спасибо.


person NawaMan    schedule 23.10.2009    source источник
comment
+1, я бы хотел, чтобы на SO было больше таких вопросов.   -  person Tim Post♦    schedule 24.10.2009


Ответы (3)


Тангенциальные комментарии

Эти первые пункты не имеют отношения к заданным вами вопросам, но ...

// Execute
void (*NativeFunction)(char*, char*) = &Plus;
NativeFunction(Params, Result); // Call the function

Я думаю, вам, вероятно, следует использовать здесь 'void *' вместо 'char *'. У меня также было бы typedef для типа указателя функции:

typedef void (*Operator)(void *params, void *result);

Тогда вы можете написать:

Operator NativeFunction = Plus;

Фактическая функция тоже будет изменена, но очень незначительно:

void Plus(void *pParams, void *pResult)

Кроме того, у вас есть небольшая проблема с именованием - это функция IntPlusDoubleGivesDouble (), а не функция общего назначения «добавить любые два типа».


Прямые ответы на вопросы

1). Если я знаю размер типов, есть ли способ изменить функцию push и pull для точной синхронизации с заполнением структуры? (измените, чтобы компилятор позаботился об этом, как о задачах Datasize и Endians).

Нет простого способа сделать это. Например, рассмотрите:

struct Type1
{
     unsigned char byte;
     int           number;
};
struct Type2
{
     unsigned char byte;
     double        number;
};

На некоторых архитектурах (например, 32-битный или 64-битный SPARC) структура Type1 будет иметь «число», выровненное по 4-байтовой границе, но структура Type2 будет иметь «номер», выровненный по 8-байтовой границе ( и может иметь «длинный двойной» на 16-байтовой границе). Ваша стратегия «проталкивать отдельные элементы» будет увеличивать указатель стека на 1 после нажатия значения «байта», поэтому вы захотите переместить указатель стека на 3 или 7 перед нажатием «числа», если указатель стека еще не установлен должным образом. выровнен. Частью вашего описания виртуальной машины будут необходимые выравнивания для любого данного типа; соответствующий код толкания должен будет гарантировать правильное выравнивание перед нажатием.

2). Если я упаковываю структуру по одной (используя #pragma pack (1)); (2.1) Будет ли приемлемо снижение производительности? и (2.2) Будет ли под угрозой стабильность программы?

На машинах x86 и x86_64, если вы упаковываете данные, вы понесете снижение производительности из-за несогласованного доступа к данным. На таких машинах, как SPARC или PowerPC (согласно mecki), вы получите ошибку шины или что-то подобное вместо этого - вы должны получить доступ к данным при их правильном выравнивании. Вы можете сэкономить место в памяти за счет снижения производительности. Лучше обеспечить производительность (что здесь включает в себя «правильную работу вместо сбоев») с минимальными затратами в космосе.

3). Как насчет заполнения 2,4 или 8? Что должно быть хорошо для общей 32- или 64-битной системы?

В SPARC вам нужно добавить N-байтовый базовый тип к N-байтовой границе. На x86 вы получите лучшую производительность, если сделаете то же самое.

4). Можете ли вы подсказать мне документацию по точному алгоритму заполнения, скажем, для GCC на x86?

Вам необходимо прочитать руководство.

5). Есть способ лучше?

Обратите внимание, что трюк 'Type1' с одним символом, за которым следует тип, дает вам требование выравнивания - возможно, с использованием макроса 'offsetof ()' из <stddef.h>:

offsetof(struct Type1, number)

Что ж, я бы не стал упаковывать данные в стек - я бы работал с нативным выравниванием, потому что он настроен на максимальную производительность. Писатель компилятора не добавляет пустяков в структуру; они поместили его туда, потому что он «лучше всего подходит» для архитектуры. Если вы решите, что знаете лучше, вы можете ожидать обычных последствий - более медленные программы, которые иногда терпят неудачу и не так переносимы.

Я также не уверен, что буду писать код в операторных функциях, предполагающий, что стек содержит структуру. Я бы вынул значения из стека с помощью аргумента Params, зная, каковы правильные смещения и типы. Если бы я вставил целое число и двойное число, я бы вытащил целое число и двойное (или, может быть, в обратном порядке - я бы вытащил двойное и целое число). Если вы не планируете необычную виртуальную машину, у некоторых функций будет много аргументов.

person Jonathan Leffler    schedule 24.10.2009
comment
Мне это не кажется касательным, это именно то, о чем просил плакат. (Или ведет к тому, о чем просили) - person Nicholas Jordan; 24.10.2009
comment
Спасибо, что ответили :-D. Так что, похоже, это нелегко. Я посмотрю на offsetof Macro, чтобы узнать, что я могу с этим поделать. Кроме того, я не думаю, что знаю, что смогу сделать это лучше. Дело в том, что код виртуальной машины должен передавать данные неизвестной ему функции (под этим я подразумеваю виртуальную машину, но функция известна компиляции кода, запущенного на виртуальной машине, выполняющей собственную функцию). Эта передача параметра должна быть обобщена и легко доступна для нативного кода (чтобы уменьшить ошибку, возникающую из-за сложной процедуры доступа к нему, и поэтому я использую структуру). - person NawaMan; 24.10.2009
comment
Еще одна вещь, которую я хочу добавить, это то, что я планирую использовать это как способ доступа к существующей нативной библиотеке, например, fopen, fputs и так далее. Операторы могут обрабатываться виртуальной машиной напрямую, и это лучше всего, поскольку компилятор обеспечит правильную работу над ней. Еще раз спасибо за ваш ответ. Я постараюсь посмотреть, что я могу сделать с помощью offsetof и, возможно, другого способа делегировать эту работу встроенного доступа компилятору. Радость!! : D - person NawaMan; 24.10.2009
comment
Вы не получаете ошибку шины на PPC; PPC может справиться с перекосом (PPC - это не Motorola 68000), это просто снижает производительность. На x86 потеря производительности довольно небольшая, кстати, намного меньше, чем на большинстве других платформ. И если вы используете упакованный атрибут GCC и получаете доступ к структуре только через поля структуры, GCC генерирует код, который гарантирует, что никогда не произойдет смещенное чтение (например, чтение поля в несколько шагов вместо одного) - person Mecki; 25.01.2011

Интересный пост и показывает, что вы много поработали. Почти идеальный ТАК пост.

У меня нет готовых ответов, так что потерпите меня. Придется задать еще несколько вопросов: P

1). Если я знаю размер типов, есть ли способ изменить функцию push и pull для точной синхронизации с заполнением структуры? (измените, чтобы компилятор позаботился об этом, как о задачах Datasize и Endians).

Это только с точки зрения производительности? Планируете ли вы ввести указатели вместе с собственными арифметическими типами?

2). Если я упаковываю структуру по одной (используя #pragma pack (1)); (2.1) Будет ли приемлемо снижение производительности? и (2.2) Будет ли под угрозой стабильность программы?

Это вещь, определяемая реализацией. Не то, на что можно рассчитывать на разных платформах.

3). Как насчет заполнения 2,4 или 8? Что должно быть хорошо для общей 32- или 64-битной системы?

Значение, соответствующее исходному размеру слова, должно обеспечить оптимальную производительность.

4). Можете ли вы дать мне документацию по точному алгоритму заполнения, например, для GCC на x86?

Я не знаю ни одной вершины моей головы. Но я видел, что используется код, похожий на this.

Обратите внимание, что вы можете указать атрибуты переменные с использованием GCC (в котором также есть функция под названием default_struct __attribute__((packed)), отключающая заполнение).

person dirkgently    schedule 23.10.2009
comment
Спасибо за ваш пост. : D 1). Как я уже сказал, хорошая производительность - это хорошо, но меня больше всего беспокоит то, что я испорчу память. : p Что касается указателя, я обязательно добавлю туда (если все будет хорошо). 2). Это плохая новость. 3). Я тоже так думаю. 4). По ссылкам посмотрю. Еще раз спасибо: D - person NawaMan; 24.10.2009

Здесь есть несколько очень хороших вопросов, многие из них связаны с некоторыми важными проблемами дизайна, но для большинства из нас - мы можем видеть, над чем вы работаете (просто опубликовано, когда я пишу, чтобы вы могли видеть, что вы вызываете интерес) мы может достаточно хорошо понимать свой английский, поэтому вы работаете над некоторыми проблемами компилятора и некоторыми проблемами языкового дизайна - становится трудно решить вопрос, но в том, что вы уже работаете в JNI, есть надежда ...

Во-первых, я бы попытался уйти от прагм; Многие, очень многие не согласятся с этим. Для канонического обсуждения того, почему см. Обоснование позиции языка D. Во-вторых, в вашем коде похоронен 16-битный указатель.

Проблемы почти бесконечны, хорошо изучены и, вероятно, похоронят нас в оппозиции и внутренней непримиримости. если я могу предложить прочитать домашнюю страницу Кеннета Лаудена, а также руководство по архитектуре Intel. Он у меня есть, я пытался его прочитать. Выравнивание структуры данных, а также многие другие вопросы, которые вы ставите на обсуждение, глубоко укоренились в исторической науке о компиляторах и, вероятно, вас затопят неизвестно чем. (жаргон или идиоматика для непредвиденных последствий)

С учетом сказанного, вот и:

  1. Размеры шрифта C Какие размеры шрифта?
  2. Инженер-компьютерщик до перехода к инженеру-программисту Вы когда-нибудь изучали микроконтроллеры? Вы можете взглянуть на некоторые работы Дона Ланкастера.
  3. Паскаль, Delphi, а теперь и программист на Java и PHP. Они сравнительно удалены из базовой фундаментальной архитектуры процессоров, хотя многие люди покажут или попытаются показать, как их можно использовать для написания мощных и фундаментальных рутины. Я предлагаю взглянуть на синтаксический анализатор рекурсивного спуска Дэвида Эка, чтобы понять, как именно начать изучение вопроса. Кроме того, у Кеннета Лаудена есть реализация "Tiny", которая является настоящим компилятором. Не так давно я нашел кое-что, что, как мне кажется, называлось asm dot org ... очень продвинутая, очень мощная работа была доступна для изучения, но это долгий путь, чтобы начать писать на ассемблере, намереваясь попасть в науку о компиляторах. Кроме того, у большинства архитектур есть различия, которые не совпадают от одного процессора к другому.
  4. доступ к существующей библиотеке

Вокруг много библиотек, у Java есть хорошие. Насчет остальных не знаю. Один из подходов - попытаться написать библиотеку. Java имеет хорошую основу и оставляет возможность людям попытаться придумать что-нибудь получше. Начните с улучшения Кнута-Морриса-Пратта или чего-то в этом роде: просто нет недостатка в местах, с которых можно начать. Попробуйте Каталог алгоритмов компьютерного программирования и обязательно посмотрите Словарь алгоритмов и структур данных в NIST

  1. always_inline

Не обязательно, см. Дов Булка - этот работник имеет докторскую степень в области CS, а также является опытным автором в областях, где временная эффективность / надежность-надежность и т. Д. Не подпадают под какую-то парадигму «бизнес-модели», из которой мы получаем из «О! Это не имеет значения» по вопросам, которые действительно имеют значение.

В заключение отметим, что приборы и средства управления составляют более 60% фактического рынка совершенных навыков программирования, как вы описываете. Почему-то в основном мы слышим о бизнес-модели. Позвольте мне поделиться с вами лакомым кусочком, который я получил из надежного источника. От 10% до 60% и более фактический риск для безопасности и имущества возникает из-за проблем с автомобилем, а не из-за кражи со взломом и тому подобного. Вы никогда не услышите призывов к «90-дневному обогащению минералов на предприятии по добыче полезных ископаемых в округе!» Что касается штрафов за нарушение правил дорожного движения, то на самом деле большинство людей даже не подозревают, что штрафы за нарушение правил дорожного движения относятся к проступку 4 класса (Северная Америка - США) и могут быть классифицированы как таковые.

Мне кажется, вы сделали хороший шаг к хорошей работе ...

person Nicholas Jordan    schedule 23.10.2009
comment
Спасибо за публикацию. : D Насчет 16-но указателя, если вы говорите о поле указателя в стеке, то это указатель смещения. Я надеюсь, что это должно сработать для стека. Что касается встроенного, кажется, что это единственный способ для меня заставить встроенную функцию работать, когда C99 включен. И привет, спасибо за ссылки тоже. - person NawaMan; 24.10.2009
comment
Это будет слишком сложно, меня только что забили за что-то вроде того, как заставить кошек следовать за скрипящим консервным ножом - слишком многие люди не проделали бы всю глубину работы, посмотрите книгу Дова Булки и почитайте на ипп ... in-lining и куча других вопросов действительно выявляются только после изучения науки о компиляторах. Прочтите книгу Дова Булки и все, что угодно в издании Addison-Wesley Professional, на обложке которого есть слово «Эффективно». Мы не собираемся выдавать достаточно информации с помощью инструмента для написания комментариев. - person Nicholas Jordan; 24.10.2009
comment
NawaMan - я думал об этом, и я хочу, чтобы вы получили копию книги «Современная криптография» Венбо Мао (отпечаток hp) ISBN: 0-13-066943-1 и прочитали, что этот человек сказал в предисловии о вещи такими, какие они есть. У большинства библиотек есть способ найти для вас книгу - это название является внутренней публикацией крупного поставщика оборудования. Кроме того, получите копию языка программирования D (Андрей Александреску) Addison-Wesley Professional ... Я вижу, к чему вы идете, вам нужно сделать небольшой шаг назад и пересмотреть обзор. - person Nicholas Jordan; 27.10.2009
comment
Спасибо Николас, посмотрю на них :-D. - person NawaMan; 28.10.2009