Quels problèmes d’alignement limitent l’utilisation d’un bloc de mémoire créé par malloc?

J’écris une bibliothèque pour divers calculs mathématiques en C. Plusieurs d’entre eux ont besoin d’espace «scratch» – mémoire utilisée pour les calculs intermédiaires. L’espace requirejs dépend de la taille des entrées, il ne peut donc pas être alloué de manière statique. La bibliothèque sera généralement utilisée pour effectuer plusieurs itérations du même type de calcul avec les mêmes entrées de taille. Je préférerais donc ne pas malloc et free à l’intérieur de la bibliothèque pour chaque appel; il serait beaucoup plus efficace d’allouer une fois assez grand un bloc, de le réutiliser pour tous les calculs, puis de le libérer.

Ma stratégie est de demander un pointeur void à un seul bloc de mémoire, éventuellement accompagné d’une fonction d’allocation. Dis quelque chose comme ça:

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

L’idée est que si l’utilisateur a l’intention d’effectuer plusieurs calculs de la même taille, il peut utiliser la fonction allocate pour saisir un bloc suffisamment volumineux, puis utiliser le même bloc de mémoire pour effectuer le calcul pour chacune des entrées. La fonction allocate n’est pas ssortingctement nécessaire, mais elle simplifie l’interface et facilite la modification future des exigences de stockage, sans que chaque utilisateur de la bibliothèque n’ait besoin de savoir exactement combien d’espace est requirejs.

Dans de nombreux cas, le bloc de mémoire dont j’ai besoin est juste un grand tableau de type double , sans aucun problème. Mais dans certains cas, j’ai besoin de types de données mélangés – disons un bloc de doubles ET un bloc d’entiers. Mon code doit être portable et doit être conforme à la norme ANSI. Je sais qu’il est correct de convertir un pointeur void dans un autre type de pointeur, mais je suis préoccupé par les problèmes d’alignement si j’essaie d’utiliser le même bloc pour deux types.

Donc, exemple spécifique. Dites que j’ai besoin d’un bloc de 3 double s et 5 int s. Puis-je implémenter mes fonctions comme ceci:

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

Est-ce légal? L’alignement fonctionne probablement bien dans cet exemple, mais si je le permute et que je prends le bloc int premier et le bloc double deuxième, cela modifiera l’alignement des double (en supposant que les doubles 64 bits et les ints 32 bits) ). Y a-t-il une meilleure manière de faire cela? Ou une approche plus standard que je devrais envisager?

Mes objectives principaux sont les suivants:

  • Si possible, j’aimerais utiliser un seul bloc pour que l’utilisateur n’ait pas à gérer plusieurs blocs ou un nombre variable de blocs requirejs.
  • J’aimerais que le bloc soit un bloc valide obtenu par malloc afin que l’utilisateur puisse appeler free fois l’opération terminée. Cela signifie que je ne veux pas faire quelque chose comme créer une petite struct avec des pointeurs sur chaque bloc, puis allouer chaque bloc séparément, ce qui nécessiterait une fonction de destruction spéciale; Je suis prêt à le faire si c’est la “seule” façon.
  • Les algorithmes et les exigences en matière de mémoire peuvent changer. J’essaie donc d’utiliser la fonction allocate afin que les versions futures puissent obtenir différentes quantités de mémoire pour des types de données potentiellement différents, sans rupture de la compatibilité ascendante.

Peut-être que cette question est traitée dans la norme C, mais je n’ai pas pu la trouver.

Si l’utilisateur appelle la fonction d’allocation de votre bibliothèque, il doit alors appeler la fonction de libération de votre bibliothèque. C’est une conception d’interface très typique (et bonne).

Donc, je dirais qu’il suffit d’aller avec la structure de pointeurs vers différents pools pour vos différents types. C’est propre, simple et portable, et quiconque lit votre code verra exactement ce que vous faites.

Si cela ne vous dérange pas de perdre de la mémoire et d’insister sur un seul bloc, vous pouvez créer une union avec tous vos types, puis allouer un tableau de ceux-ci …

Essayer de trouver la mémoire correctement alignée dans un bloc massif n’est qu’un gâchis. Je ne suis même pas sûr que vous puissiez le faire de manière portable. Quel est le plan? Jetez des pointeurs sur intptr_t , faites des arrondis, puis intptr_t un pointeur?

La mémoire d’un seul malloc peut être partitionnée pour être utilisée dans plusieurs baies, comme indiqué ci-dessous.

Supposons que nous voulions des tableaux de types A, B et C avec des éléments NA, NB et NC. Nous faisons ceci:

 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 est:

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

Comme indiqué par R .. , cela utilise le fait que la taille d’un type doit être un multiple de l’exigence d’alignement pour ce type. En C 2011, sizeof dans les appels RoundUp peut être modifié en _Alignof . Cela peut économiser une petite quantité d’espace lorsque l’exigence d’alignement d’un type est inférieure à sa taille.

La dernière norme C11 a le type max_align_t (et le spécificateur _Alignof et l’opérateur _Alignof et l’en-tête ).

Le compilateur GCC a une macro __BIGGEST_ALIGNMENT__ (donnant l’alignement de taille maximale). Cela prouve également certaines extensions liées à l’ alignement .

Souvent, utiliser 2*sizeof(void*) (en tant que plus grand alignement pertinent) est en pratique assez sûr (du moins sur la plupart des systèmes dont j’ai entendu parler ces jours-ci; mais on pourrait imaginer des processeurs et des systèmes étranges où ce n’est pas le cas , peut-être quelques DSP ). Pour être sûr, étudiez les détails de ABI et les conventions d’appel de votre implémentation particulière, par exemple les conventions d’appel x86-64 ABI et x86 …

Et le système malloc est garanti pour renvoyer un pointeur suffisamment aligné (à toutes fins utiles).

Sur certains systèmes et cibles, ainsi que sur certains processeurs offrant un alignement plus large, les performances peuvent être améliorées (notamment lorsque le compilateur doit être optimisé). Vous devrez peut-être (ou vouloir) en informer le compilateur, par exemple sur GCC en utilisant des atsortingbuts variables …

N’oubliez pas que selon Fulton

les logiciels portables n’existent pas, il n’y a que les logiciels portés.

mais intptr_t et max_align_t sont là pour vous aider ….

Notez que l’alignement requirejs pour tout type doit diviser de manière égale la taille du type; c’est une conséquence de la représentation des types de tableaux. Ainsi, en l’absence de fonctions C11 permettant de déterminer l’alignement requirejs pour un type, vous pouvez simplement effectuer une estimation prudente et utiliser la taille du type. En d’autres termes, si vous souhaitez utiliser une partie de l’allocation de malloc pour stocker double doublets, assurez-vous qu’elle commence par un décalage multiple de sizeof(double) .