Управление памятью C для кроссплатформенной виртуальной машины - PullRequest
6 голосов
/ 24 октября 2009

Я задал вопрос о размерах 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;
}

Push, pull и вызов нативной функции могут быть инициированы байтовым кодом (так будет создаваться VM).

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

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));
}

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

<code>// 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;</p>

<p>// 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);
}</p>

<p>// 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 32bit GCC-C99). Будет очень хорошо, если это будет работать и на других ОС / Архитектура. Но есть, по крайней мере, три проблемы, связанные с памятью, о которых мы должны знать.

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

2). Endianness - то же самое с размером данных.

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

Мои вопросы:

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

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

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

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

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

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

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

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

Ответы [ 3 ]

2 голосов
/ 24 октября 2009

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

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

// 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 после нажатия значения «byte» - так что вы захотите переместить указатель стека на 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, зная, какие были правильные смещения и типы. Если бы я нажал на целое и двойное число, то я бы вытащил целое и двойное число (или, может быть, в обратном порядке - я бы вытащил двойное и целое). Если вы не планируете необычную виртуальную машину, у нескольких функций будет много аргументов.

1 голос
/ 24 октября 2009

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

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

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

С этим сказано, вот идет:

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

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

  1. always_inline

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

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

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

1 голос
/ 24 октября 2009

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

У меня нет готовых ответов, поэтому, пожалуйста, потерпите меня. Мне придется задать еще несколько вопросов: P

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

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

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

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

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

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

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

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

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

...