Какие проблемы с выравниванием ограничивают использование блока памяти, созданного malloc?

Я пишу библиотеку для различных математических вычислений в C. Для некоторых из них требуется некоторая «царапающая» память пространства, которая используется для промежуточных вычислений. Требуемое пространство зависит от размера входов, поэтому его нельзя статически выделять. Библиотека обычно используется для выполнения множества итераций одного и того же типа вычислений с входами того же размера, поэтому я бы предпочел не делать malloc и free внутри библиотеки для каждого вызова; было бы гораздо эффективнее выделить достаточно большой блок один раз, повторно использовать его для всех вычислений, а затем освободить его.

Моя планируемая страtagsя – запросить указатель на void одного блока памяти, возможно, сопутствующей функции распределения. Скажем, что-то вроде этого:

 void *allocateScratch(size_t rows, size_t columns); void doCalculation(size_t rows, size_t columns, double *data, void *scratch); 

Идея состоит в том, что если пользователь намеревается выполнить несколько вычислений одного и того же размера, он может использовать функцию allocate для захвата блока, который является достаточно большим, а затем использовать тот же блок памяти для выполнения вычисления для каждого из входов. Функция allocate не является строго необходимой, но упрощает интерфейс и упрощает изменение требований к хранилищу в будущем, без необходимости каждого пользователя библиотеки, чтобы точно знать, сколько места требуется.

Во многих случаях блок памяти, который мне нужен, представляет собой просто большой массив типа double , без проблем. Но в некоторых случаях мне нужны смешанные типы данных – скажем, блок удвоений И блок целых чисел. Мой код должен быть портативным и должен соответствовать стандарту ANSI. Я знаю, что нормально void указатель void на любой другой тип указателя, но я беспокоюсь о проблемах выравнивания, если попытаюсь использовать один и тот же блок для двух типов.

Итак, конкретный пример. Скажем, мне нужен блок из 3 double с и 5 в. Могу ли я реализовать свои функции следующим образом:

 void *allocateScratch(...) { return malloc(3 * sizeof(double) + 5 * sizeof(int)); } void doCalculation(..., void *scratch) { double *dblArray = scratch; int *intArray = ((unsigned char*)scratch) + 3 * sizeof(double); } 

Является ли это законным? В этом примере выравнивание, вероятно, работает нормально, но что, если я его переключу и сначала возьму блок int а второй – второй, что приведет к смещению выравнивания double (предполагая 64-разрядные удвоения и 32-битные ints ). Есть лучший способ сделать это? Или более стандартный подход, который я должен рассмотреть?

Мои самые большие цели заключаются в следующем:

  • Я бы хотел использовать один блок, если это возможно, поэтому пользователю не нужно иметь дело с несколькими блоками или с изменением количества требуемых блоков.
  • Я хотел бы, чтобы блок был действительным блоком, полученным malloc поэтому пользователь может позвонить free завершении. Это означает, что я не хочу делать что-то вроде создания небольшой struct которая имеет указатели на каждый блок, а затем выделяет каждый блок отдельно, что потребует специальной функции destroy; Я готов сделать это, если это единственный способ.
  • Алгоритмы и требования к памяти могут меняться, поэтому я пытаюсь использовать функцию allocate, чтобы будущие версии могли получать разные объемы памяти для потенциально разных типов данных без нарушения обратной совместимости.

Возможно, эта проблема решена в стандарте C, но я не смог ее найти.

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

Поэтому я бы сказал, просто перейдите со структурой указателей в разные пулы для разных типов. Это чистый, простой и портативный, и любой, кто читает ваш код, будет точно видеть, что вы делаете.

Если вы не против потерять память и настаивать на одном блоке, вы можете создать объединение со всеми вашими типами, а затем выделить массив из этих …

Попытка найти соответствующую выровненную память в массивном блоке – это просто беспорядок. Я даже не уверен, что вы можете сделать это переносимо. Каков план? intptr_t указатели на intptr_t , выполните округление, затем верните указатель?

Память одного malloc может быть разделена на несколько массивов, как показано ниже.

Предположим, нам нужны массивы типов A, B и C с элементами NA, NB и NC. Мы делаем это:

 size_t Offset = 0; ptrdiff_t OffsetA = Offset; // Put array at current offset. Offset += NA * sizeof(A); // Move offset to end of array. Offset = RoundUp(Offset, sizeof(B)); // Align sufficiently for type. ptrdiff_t OffsetB = Offset; // Put array at current offset. Offset += NB * sizeof(B); // Move offset to end of array. Offset = RoundUp(Offset, sizeof(C)); // Align sufficiently for type. ptrdiff_t OffsetC = Offset; // Put array at current offset. Offset += NC * sizeof(C); // Move offset to end of array. unsigned char *Memory = malloc(Offset); // Allocate memory. // Set pointers for arrays. A *pA = Memory + OffsetA; B *pB = Memory + OffsetB; C *pC = Memory + OffsetC; 

где RoundUp :

 // Return Offset rounded up to a multiple of Size. size_t RoundUp(size_t Offset, size_t Size) { size_t x = Offset + Size - 1; return x - x % Size; } 

Это использует факт, как отмечено R .. , что размер типа должен быть кратным требованию выравнивания для этого типа. В C 2011 sizeof в вызовах _Alignof может быть изменен на _Alignof , и это может сэкономить небольшое пространство, когда требование выравнивания типа меньше его размера.

Последний стандарт C11 имеет тип max_align_t_Alignas и _Alignof и заголовок ).

Компилятор GCC имеет макрос __BIGGEST_ALIGNMENT__ (с максимальным выравниванием по размеру). Он также доказывает некоторые расширения, связанные с выравниванием .

Часто использование 2*sizeof(void*) (как самого большого соответствующего выравнивания) на практике совершенно безопасно (по крайней мере, на большинстве систем, которые я слышал об этих днях, но можно представить себе странные процессоры и системы, где это не так , возможно, некоторые DSP- файлы). Разумеется, изучите детали ABI и вызывающие соглашения вашей конкретной реализации, например, соглашения о вызовах x86-64 ABI и x86 …

И система malloc гарантированно вернет достаточно выровненный указатель (для всех целей).

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

Не забывайте, что согласно Фултону

нет ничего такого, как портативное программное обеспечение, только программное обеспечение, которое было перенесено.

но intptr_t и max_align_t здесь, чтобы помочь вам ….

Обратите внимание, что требуемое выравнивание для любого типа должно равномерно делить размер типа; это является следствием представления типов массивов. Таким образом, в отсутствие возможностей C11 для определения требуемого выравнивания для типа вы можете просто оценить консервативно и использовать размер этого типа. Другими словами, если вы хотите вырезать часть выделения из malloc для использования сохранения double s, убедитесь, что она начинается со смещения, которое кратно sizeof(double) .