Je viens de terminer un test dans le cadre d'un entretien d'embauche, et une question m'a dérouté, même en utilisant Google comme référence. J'aimerais voir ce que l'équipe StackOverflow peut en faire:
La
memset_16aligned
fonction nécessite un pointeur aligné de 16 octets qui lui est transmis, sinon elle se bloque.a) Comment alloueriez-vous 1024 octets de mémoire et l'alignez-vous sur une limite de 16 octets?
b) Libérez la mémoire après l'memset_16aligned
exécution de.
{
void *mem;
void *ptr;
// answer a) here
memset_16aligned(ptr, 0, 1024);
// answer b) here
}
c
memory-management
JimDaniel
la source
la source
Réponses:
Réponse originale
Réponse fixe
Explication comme demandé
La première étape consiste à allouer suffisamment d'espace libre, au cas où. Étant donné que la mémoire doit être alignée sur 16 octets (ce qui signifie que l'adresse d'octet de tête doit être un multiple de 16), l'ajout de 16 octets supplémentaires garantit que nous avons suffisamment d'espace. Quelque part dans les 16 premiers octets, il y a un pointeur aligné de 16 octets. (Notez que
malloc()
est censé renvoyer un pointeur qui est suffisamment bien aligné pour tout . But Cependant, le sens de « tout » est principalement pour des choses comme les types de base -long
,double
,long double
,long long
., Et des pointeurs vers des objets et des pointeurs vers des fonctions Lorsque vous êtes faire des choses plus spécialisées, comme jouer avec des systèmes graphiques, ils peuvent avoir besoin d'un alignement plus strict que le reste du système - d'où des questions et réponses comme celle-ci.)L'étape suivante consiste à convertir le pointeur void en un pointeur char; Malgré GCC, vous n'êtes pas censé faire de l'arithmétique des pointeurs sur les pointeurs vides (et GCC a des options d'avertissement pour vous dire quand vous en abusez). Ajoutez ensuite 16 au pointeur de départ. Supposons que
malloc()
vous ayez renvoyé un pointeur incroyablement mal aligné: 0x800001. L'ajout du 16 donne 0x800011. Maintenant, je veux arrondir à la limite de 16 octets - je veux donc réinitialiser les 4 derniers bits à 0. 0x0F a les 4 derniers bits mis à un; par conséquent,~0x0F
tous les bits sont définis sur un, à l'exception des quatre derniers. Et cela avec 0x800011 donne 0x800010. Vous pouvez parcourir les autres décalages et voir que la même arithmétique fonctionne.La dernière étape,
free()
est facile: vous toujours, et seulement, le retour àfree()
une valeur que l' un desmalloc()
,calloc()
ourealloc()
retourné à vous - tout est bien une catastrophe. Vous avez correctement fournimem
de conserver cette valeur - merci. Le libre le libère.Enfin, si vous connaissez les éléments internes du
malloc
package de votre système , vous pourriez deviner qu'il pourrait bien renvoyer des données alignées sur 16 octets (ou qu'il pourrait être aligné sur 8 octets). S'il était aligné sur 16 octets, vous n'auriez pas besoin de vous arrêter aux valeurs. Cependant, c'est douteux et non portable - d'autresmalloc
packages ont des alignements minimaux différents, et donc supposer une chose quand il fait quelque chose de différent conduirait à des vidages de mémoire. Dans de larges limites, cette solution est portable.Quelqu'un d'autre a mentionné
posix_memalign()
une autre façon d'obtenir la mémoire alignée; qui n'est pas disponible partout, mais pourrait souvent être implémenté en utilisant cela comme base. Notez qu'il était pratique que l'alignement soit une puissance de 2; d'autres alignements sont plus compliqués.Encore un commentaire - ce code ne vérifie pas que l'allocation a réussi.
Amendement
Le programmeur Windows a souligné que vous ne pouvez pas effectuer d'opérations de masquage de bits sur des pointeurs, et, en effet, GCC (testé 3.4.6 et 4.3.1) se plaint comme ça. Ainsi, une version modifiée du code de base - converti en un programme principal, suit. J'ai également pris la liberté d'ajouter seulement 15 au lieu de 16, comme cela a été souligné. J'utilise
uintptr_t
depuis que C99 existe depuis assez longtemps pour être accessible sur la plupart des plateformes. Si ce n'était pas pour l'utilisation dePRIXPTR
dans lesprintf()
instructions, ce serait suffisant pour#include <stdint.h>
au lieu d'utiliser#include <inttypes.h>
. [Ce code inclut le correctif signalé par CR , qui réitère un point soulevé par Bill K il y a plusieurs années, que j'ai réussi à ignorer jusqu'à présent.]Et voici une version légèrement plus généralisée, qui fonctionnera pour les tailles qui sont une puissance de 2:
Pour convertir
test_mask()
en une fonction d'allocation à usage général, la valeur de retour unique de l'allocateur devrait coder l'adresse de libération, comme plusieurs personnes l'ont indiqué dans leurs réponses.Problèmes avec les enquêteurs
Uri a commenté: Peut-être que j'ai un problème de compréhension de la lecture ce matin, mais si la question d'entrevue dit spécifiquement: "Comment alloueriez-vous 1024 octets de mémoire" et vous allouez clairement plus que cela. Ne serait-ce pas un échec automatique de l'intervieweur?
Ma réponse ne rentre pas dans un commentaire de 300 caractères ...
Cela dépend, je suppose. Je pense que la plupart des gens (y compris moi) ont pris la question pour signifier "Comment alloueriez-vous un espace dans lequel 1024 octets de données peuvent être stockés, et où l'adresse de base est un multiple de 16 octets". Si l'intervieweur voulait vraiment savoir comment allouer 1024 octets (uniquement) et l'aligner sur 16 octets, alors les options sont plus limitées.
Cependant, si l'intervieweur s'attendait à l'une de ces réponses, je m'attendrais à ce qu'il reconnaisse que cette solution répond à une question étroitement liée, puis à recadrer sa question pour orienter la conversation dans la bonne direction. (De plus, si l'intervieweur est devenu très bâclé, alors je ne voudrais pas le travail; si la réponse à une exigence insuffisamment précise est abattue dans les flammes sans correction, alors l'intervieweur n'est pas quelqu'un pour qui il est sûr de travailler.)
Le monde continue
Le titre de la question a changé récemment. C'est Résoudre l'alignement de la mémoire dans la question d'entrevue C qui m'a déconcerté . Le titre révisé ( Comment allouer la mémoire alignée uniquement en utilisant la bibliothèque standard? ) Exige une réponse légèrement révisée - cet addendum le fournit.
C11 (ISO / IEC 9899: 2011) fonction ajoutée
aligned_alloc()
:Et POSIX définit
posix_memalign()
:L'un ou l'autre ou les deux peuvent être utilisés pour répondre à la question maintenant, mais seule la fonction POSIX était une option lorsque la question a été répondue à l'origine.
Dans les coulisses, la nouvelle fonction de mémoire alignée fait à peu près le même travail que celui décrit dans la question, sauf qu'elle a la possibilité de forcer l'alignement plus facilement et de garder une trace du début de la mémoire alignée en interne afin que le code ne avoir à traiter spécialement - il libère juste la mémoire retournée par la fonction d'allocation qui a été utilisée.
la source
<inttypes.h>
de C99 disponible (au moins pour la chaîne de format - sans doute, les valeurs devraient être passées avec un cast :)(uintptr_t)mem, (uintptr_t)ptr
. La chaîne de format repose sur la concaténation de chaînes et la macro PRIXPTR est leprintf()
spécificateur de longueur et de type correct pour la sortie hexadécimale d'uneuintptr_t
valeur. L'alternative est d'utiliser,%p
mais la sortie de celui-ci varie selon la plate-forme (certains ajoutent un interligne0x
, la plupart n'en ont pas) et est généralement écrite avec des chiffres hexadécimaux en minuscule, ce que je n'aime pas; ce que j'ai écrit est uniforme sur toutes les plateformes.Trois réponses légèrement différentes selon la façon dont vous regardez la question:
1) La solution de Jonathan Leffler est assez bonne pour la question exacte, sauf que pour arrondir à 16 alignés, vous n'avez besoin que de 15 octets supplémentaires, pas de 16.
UNE:
B:
2) Pour une fonction d'allocation de mémoire plus générique, l'appelant ne veut pas avoir à suivre deux pointeurs (un à utiliser et un à libérer). Vous stockez donc un pointeur vers le "vrai" tampon sous le tampon aligné.
UNE:
B:
Notez que contrairement à (1), où seulement 15 octets ont été ajoutés à mem, ce code pourrait en fait réduire l'alignement si votre implémentation arrive à garantir un alignement de 32 octets à partir de malloc (peu probable, mais en théorie une implémentation C pourrait avoir un 32 octets type aligné). Cela n'a pas d'importance si tout ce que vous faites est d'appeler memset_16aligned, mais si vous utilisez la mémoire pour une structure, cela peut être important.
Je ne suis pas sûr de savoir ce qu'est un bon correctif pour cela (autre que d'avertir l'utilisateur que le tampon renvoyé n'est pas nécessairement adapté aux structures arbitraires) car il n'y a aucun moyen de déterminer par programme ce qu'est la garantie d'alignement spécifique à l'implémentation. Je suppose qu'au démarrage, vous pouvez allouer deux ou plusieurs tampons de 1 octet et supposer que le pire alignement que vous voyez est l'alignement garanti. Si vous vous trompez, vous perdez de la mémoire. Quiconque a une meilleure idée, dites-le s'il vous plaît ...
[ Ajouté : L'astuce «standard» consiste à créer une union de «types susceptibles d'être alignés au maximum» pour déterminer l'alignement requis. Les types alignés au maximum sont susceptibles d'être (en C99) '
long long
', 'long double
', 'void *
' ou 'void (*)(void)
'; si vous incluez<stdint.h>
, vous pourriez probablement utiliser 'intmax_t
' à la place delong long
(et, sur les machines Power 6 (AIX), vousintmax_t
obtiendriez un type entier 128 bits). Les exigences d'alignement pour cette union peuvent être déterminées en l'incorporant dans une structure avec un seul caractère suivi de l'union:Vous utiliseriez alors le plus grand de l'alignement demandé (dans l'exemple, 16) et la
align
valeur calculée ci-dessus.Sur Solaris 10 (64 bits), il semble que l'alignement de base du résultat
malloc()
est un multiple de 32 octets.]
En pratique, les allocateurs alignés prennent souvent un paramètre pour l'alignement plutôt que d'être câblé. Ainsi, l'utilisateur transmettra la taille de la structure dont il se soucie (ou la moindre puissance de 2 supérieure ou égale à cela) et tout ira bien.
3) Utilisez ce que fournit votre plateforme:
posix_memalign
pour POSIX,_aligned_malloc
sous Windows.4) Si vous utilisez C11, l'option la plus propre - portable et concise - consiste à utiliser la fonction de bibliothèque standard
aligned_alloc
introduite dans cette version de la spécification de langage.la source
ASSERT(mem);
pour vérifier les résultats d'allocation;assert
sert à détecter les erreurs de programmation et non à manquer de ressources d'exécution.char *
et asize_t
entraînera une erreur. Il faudrait utiliser quelque chose commeuintptr_t
.Vous pouvez également essayer
posix_memalign()
(sur les plates-formes POSIX, bien sûr).la source
Voici une approche alternative à la partie «arrondir». Ce n'est pas la solution la plus brillamment codée, mais elle fait le travail, et ce type de syntaxe est un peu plus facile à retenir (plus fonctionnerait pour les valeurs d'alignement qui ne sont pas une puissance de 2). Le
uintptr_t
casting était nécessaire pour apaiser le compilateur; l'arithmétique des pointeurs n'aime pas beaucoup la division ou la multiplication.la source
Malheureusement, en C99, il semble assez difficile de garantir l'alignement de toute sorte d'une manière qui serait portable sur toute implémentation C conforme à C99. Pourquoi? Parce qu'un pointeur n'est pas garanti d'être "l'adresse d'octet" que l'on pourrait imaginer avec un modèle de mémoire plate. La représentation de uintptr_t n'est pas non plus garantie, qui est de toute façon un type facultatif.
Nous connaissons peut-être certaines implémentations qui utilisent une représentation pour void * (et par définition, aussi char * ) qui est une simple adresse d'octet, mais en C99, elle est opaque pour nous, les programmeurs. Une implémentation peut représenter un pointeur par un ensemble { segment , offset } où l' offset pourrait avoir un alignement qui sait quoi "en réalité". Pourquoi, un pointeur pourrait même être une certaine forme de valeur de recherche de table de hachage, ou même une valeur de recherche de liste liée. Il pourrait encoder des informations sur les limites.
Dans un récent projet C1X pour une norme C, nous voyons le mot clé _Alignas . Cela pourrait aider un peu.
La seule garantie que C99 nous donne est que les fonctions d'allocation de mémoire renverront un pointeur approprié pour une affectation à un pointeur pointant sur n'importe quel type d'objet. Comme nous ne pouvons pas spécifier l'alignement des objets, nous ne pouvons pas implémenter nos propres fonctions d'allocation avec la responsabilité de l'alignement d'une manière portable bien définie.
Il serait bon de se tromper sur cette affirmation.
la source
aligned_alloc()
. (C ++ 11/14 / 1z ne l'a toujours pas)._Alignas()
et C ++alignas()
ne font rien pour l'allocation dynamique, seulement pour le stockage automatique et statique (ou la structure).Sur le front de remplissage de 16 vs 15 octets, le nombre réel que vous devez ajouter pour obtenir un alignement de N est max (0, NM) où M est l'alignement naturel de l'allocateur de mémoire (et les deux sont des puissances de 2).
Étant donné que l'alignement minimal de la mémoire de tout allocateur est de 1 octet, 15 = max (0,16-1) est une réponse prudente. Cependant, si vous savez que votre allocateur de mémoire va vous donner des adresses alignées int 32 bits (ce qui est assez courant), vous auriez pu utiliser 12 comme pad.
Ce n'est pas important pour cet exemple, mais cela pourrait être important sur un système embarqué avec 12 Ko de RAM où chaque int enregistré seul compte.
La meilleure façon de l'implémenter si vous essayez réellement d'enregistrer chaque octet possible est en tant que macro afin de pouvoir l'alimenter en alignement de votre mémoire native. Encore une fois, cela n'est probablement utile que pour les systèmes embarqués où vous devez enregistrer chaque octet.
Dans l'exemple ci-dessous, sur la plupart des systèmes, la valeur 1 est très bien car
MEMORY_ALLOCATOR_NATIVE_ALIGNMENT
, pour notre système embarqué théorique avec des allocations alignées sur 32 bits, les éléments suivants pourraient économiser un tout petit peu de mémoire précieuse:la source
Peut-être auraient-ils été satisfaits d'une connaissance de memalign ? Et comme le souligne Jonathan Leffler, il y a deux nouvelles fonctions préférables à connaître.
Oups, Florin m'a battu. Cependant, si vous lisez la page de manuel à laquelle j'ai lié, vous comprendrez très probablement l'exemple fourni par une affiche précédente.
la source
memalign
fonction est obsolète etaligned_alloc
ouposix_memalign
doit être utilisée à la place". Je ne sais pas ce qu'il a dit en octobre 2008 - mais il ne l'a probablement pas mentionnéaligned_alloc()
car cela a été ajouté au C11.Nous faisons ce genre de choses tout le temps pour Accelerate.framework, une bibliothèque OS X / iOS fortement vectorisée, où nous devons faire attention à l'alignement tout le temps. Il existe plusieurs options, dont une ou deux que je n'ai pas vues mentionnées ci-dessus.
La méthode la plus rapide pour un petit tableau comme celui-ci est simplement de le coller sur la pile. Avec GCC / clang:
Pas de gratuit () requis. Il s'agit généralement de deux instructions: soustraire 1024 du pointeur de pile, puis ET le pointeur de pile avec -alignment. Vraisemblablement, le demandeur avait besoin des données sur le tas car la durée de vie de la baie dépassait la pile ou la récursivité est à l'œuvre ou l'espace de pile est très important.
Sous OS X / iOS, tous les appels vers malloc / calloc / etc. sont toujours alignés sur 16 octets. Si vous aviez besoin de 32 octets alignés pour AVX, par exemple, vous pouvez utiliser posix_memalign:
Certaines personnes ont mentionné l'interface C ++ qui fonctionne de manière similaire.
Il ne faut pas oublier que les pages sont alignées sur de grandes puissances de deux, donc les tampons alignés sur les pages sont également alignés sur 16 octets. Ainsi, mmap () et valloc () et d'autres interfaces similaires sont également des options. mmap () a l'avantage que le tampon peut être alloué préinitialisé avec quelque chose de non nul, si vous le souhaitez. Étant donné que ceux-ci ont une taille alignée sur la page, vous n'obtiendrez pas l'allocation minimale de ceux-ci, et il sera probablement soumis à un défaut de machine virtuelle la première fois que vous le toucherez.
Cheesy: Allumez guard malloc ou similaire. Les tampons de taille n * 16 octets tels que celui-ci seront alignés n * 16 octets, car la machine virtuelle est utilisée pour intercepter les dépassements et ses limites se trouvent aux limites de la page.
Certaines fonctions Accelerate.framework intègrent un tampon temporaire fourni par l'utilisateur à utiliser comme espace de travail. Ici, nous devons supposer que le tampon qui nous est transmis est très mal aligné et que l'utilisateur essaie activement de rendre notre vie difficile par dépit. (Nos cas de test collent une page de garde juste avant et après le tampon temporaire pour souligner la dépit.) Ici, nous retournons la taille minimale dont nous avons besoin pour garantir un segment aligné de 16 octets quelque part, puis alignons manuellement le tampon par la suite. Cette taille est souhaitée_taille + alignement - 1. Donc, dans ce cas, c'est 1024 + 16 - 1 = 1039 octets. Alignez ensuite comme suit:
L'ajout d'alignement-1 déplacera le pointeur au-delà de la première adresse alignée, puis AND avec -alignment (par exemple 0xfff ... ff0 pour alignement = 16) le ramènera à l'adresse alignée.
Comme décrit par d'autres articles, sur d'autres systèmes d'exploitation sans garanties d'alignement de 16 octets, vous pouvez appeler malloc avec la plus grande taille, mettre de côté le pointeur gratuitement () plus tard, puis aligner comme décrit immédiatement ci-dessus et utiliser le pointeur aligné, autant que décrit pour notre cas de tampon temporaire.
Quant à align_memset, c'est plutôt idiot. Vous n'avez qu'à boucler jusqu'à 15 octets pour atteindre une adresse alignée, puis procédez à des magasins alignés après cela avec un code de nettoyage possible à la fin. Vous pouvez même faire les bits de nettoyage en code vectoriel, soit en tant que magasins non alignés qui chevauchent la région alignée (à condition que la longueur soit au moins la longueur d'un vecteur) ou en utilisant quelque chose comme movmaskdqu. Quelqu'un est juste paresseux. Cependant, c'est probablement une question d'entrevue raisonnable si l'intervieweur veut savoir si vous êtes à l'aise avec stdint.h, les opérateurs au niveau du bit et les fondamentaux de la mémoire, de sorte que l'exemple artificiel peut être pardonné.
la source
Je suis surpris que personne n'ait voté la réponse de Shao selon laquelle, si je comprends bien, il est impossible de faire ce qui est demandé dans la norme C99, car la conversion formelle d'un pointeur en un type intégral est un comportement indéfini. (En dehors de la norme autorisant la conversion de <-> , mais la norme ne semble pas autoriser de manipulation de la valeur, puis la reconvertir.)
uintptr_t
void*
uintptr_t
la source
unsigned char* myptr
; puis calculez `mptr + = (16- (uintptr_t) my_ptr) & 0x0F, le comportement serait défini sur toutes les implémentations qui définissent my_ptr, mais si le pointeur résultant serait aligné dépendrait du mappage entre les bits uintptr_t et les adresses.l'utilisation de memalign, Aligned-Memory-Blocks peut être une bonne solution au problème.
la source
memalign
fonction est obsolète etaligned_alloc
ouposix_memalign
doit être utilisée à la place". Je ne sais pas ce qu'il a dit en octobre 2010.La première chose qui m'est venue à l'esprit lors de la lecture de cette question a été de définir une structure alignée, de l'instancier, puis de la désigner.
Y a-t-il une raison fondamentale pour laquelle je manque car personne d'autre ne l'a suggéré?
En tant que sidenote, puisque j'ai utilisé un tableau de caractères (en supposant que le caractère du système est de 8 bits (soit 1 octet)), je ne vois pas la nécessité du
__attribute__((packed))
nécessairement (corrigez-moi si je me trompe), mais je le mets de quelque manière que.Cela fonctionne sur deux systèmes sur lesquels j'ai essayé, mais il est possible qu'il existe une optimisation du compilateur que je ne suis pas au courant de me donner de faux positifs vis-à-vis de l'efficacité du code. J'ai utilisé
gcc 4.9.2
sur OSX etgcc 5.2.1
sur Ubuntu.la source
Spécifique à MacOS X:
C11 est pris en charge, vous pouvez donc simplement appeler align_malloc (16, taille).
MacOS X sélectionne du code optimisé pour les processeurs individuels au démarrage pour memset, memcpy et memmove et ce code utilise des astuces dont vous n'avez jamais entendu parler pour le rendre rapide. 99% de chances que le memset fonctionne plus rapidement que tout memset écrit à la main16, ce qui rend toute la question inutile.
Si vous voulez une solution 100% portable, avant C11 il n'y en a pas. Parce qu'il n'y a aucun moyen portable de tester l'alignement d'un pointeur. S'il ne doit pas être 100% portable, vous pouvez utiliser
Cela suppose que l'alignement d'un pointeur est stocké dans les bits les plus bas lors de la conversion d'un pointeur en entier non signé. La conversion en entier non signé perd des informations et est définie par l'implémentation, mais cela n'a pas d'importance car nous ne convertissons pas le résultat en un pointeur.
La partie horrible est bien sûr que le pointeur d'origine doit être enregistré quelque part pour appeler free () avec lui. Donc, dans l'ensemble, je doute vraiment de la sagesse de cette conception.
la source
aligned_malloc
dans OS X? J'utilise Xcode 6.1 et il n'est défini nulle part dans le SDK iOS, ni déclaré nulle part dans/usr/include/*
.aligned_alloc()
, mais elle n'est pas non plus déclarée. De GCC 5.3.0, je reçois les messages intéressantsalig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]
etalig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’
. Le code comprenait en effet<stdlib.h>
, mais ni-std=c11
ni-std=gnu11
modifié les messages d'erreur.Vous pouvez également ajouter quelques 16 octets, puis pousser le ptr d'origine à 16 bits aligné en ajoutant le (16-mod) comme sous le pointeur:
la source
S'il y a des contraintes, vous ne pouvez pas gaspiller un seul octet, alors cette solution fonctionne: Remarque: Il y a un cas où cela peut être exécuté à l'infini: D
la source
%
opérateur est défini devoid*
manière significative?Pour la solution, j'ai utilisé un concept de remplissage qui aligne la mémoire et ne gaspille pas la mémoire d'un seul octet.
S'il y a des contraintes, vous ne pouvez pas perdre un seul octet. Tous les pointeurs alloués avec malloc sont alignés sur 16 octets.
C11 est pris en charge, vous pouvez donc simplement appeler
aligned_alloc (16, size)
.la source
malloc()
est en effet aligné sur une limite de 16 octets, mais rien dans aucune norme ne garantit que - il sera simplement suffisamment bien aligné pour toute utilisation, et sur de nombreux systèmes 32 bits alignés sur un Une limite de 8 octets est suffisante, et pour certains, une limite de 4 octets est suffisante.J'espère que celui-ci est la mise en œuvre la plus simple, faites-moi part de vos commentaires.
la source
la source
add += 16 - (add % 16)
.(2 - (2 % 16)) == 0
.