C/C++ de la performance des tableaux statiques vs tableaux dynamiques
Quand la performance est essentielle pour une application, devrait-on songer à la déclaration d'un tableau sur la pile et le tas? Permettez-moi de décrire pourquoi cette question est venue à l'esprit.
Comme les tableaux en C/C++ ne sont pas des objets et de la décadence de pointeurs, le compilateur utilise l'index fourni pour effectuer l'arithmétique des pointeurs pour accéder aux éléments. Ma compréhension est que cette procédure diffère d'une manière statique déclaré tableau dynamiquement un tableau déclaré lors de la va au-delà de la première dimension.
Si je devais déclarer un tableau sur la pile comme suit;
int array[2][3] = { 0, 1, 2, 3, 4, 5 }
//In memory { row1 } { row2 }
Ce tableau devraient être stockées dans Ligne principale format en mémoire, car il est stocké dans un bloc contigu de mémoire. Cela signifie que lorsque je tente d'accéder à un élément dans le tableau, le compilateur doit effectuer certaines d'addition et de multiplication, afin de déterminer l'emplacement correct.
Donc, si je devais faire ce qui suit
int x = array[1][2]; //x = 5
Le compilateur aurait ensuite utiliser cette formule où:
i = index de ligne j = colonne d'indice n = taille d'une seule ligne (ici n = 2)
array = pointeur vers le premier élément
*(array + (i*n) + j)
*(array + (1*2) + 2)
Cela signifie que si je devais faire une boucle sur ce tableau à l'accès de chacun de ses éléments, une autre étape de multiplication est effectuée pour chaque accès par index.
Maintenant, dans un tableau déclaré sur le tas, le paradigme est différent et nécessite plusieurs étapes de la solution. Note: j'ai pu également utiliser le C++ opérateur de nouveau ici, mais je crois qu'il n'y a pas de différence dans la façon dont les données sont représentées.
int ** array;
int rowSize = 2;
//Create a 2 by 3 2d array on the heap
array = malloc(2 * sizeof(int*));
for (int i = 0; i < 2; i++) {
array[i] = malloc(3 * sizeof(int));
}
//Populating the array
int number = 0;
for (int i = 0; i < 2; i++) {
for (int j = 0l j < 3; j++) {
array[i][j] = number++;
}
}
Depuis le tableau est maintenant dynamique, sa représentation est un tableau unidimensionnel de dimensions des tableaux. Je vais essayer de dessiner un ascii image...
int * int int int
int ** array-> [0] 0 1 2
[1] 3 4 5
Cela impliquerait que la multiplication est plus impliqué droit? Si je devais faire ce qui suit
int x = array[1][1];
Ce serait alors d'effectuer indirection/l'arithmétique des pointeurs sur tableau[1] pour accéder à un pointeur vers la deuxième ligne, puis effectuer une nouvelle fois pour accéder au deuxième élément. Ai-je raison en disant cela?
Maintenant que le contexte est, de retour à la question. Si je suis à l'écriture de code pour une application qui nécessite croustillant de la performance, comme un jeu qui a autour de 0,016 secondes pour effectuer le rendu d'une image, dois-je réfléchir à deux fois au sujet de l'utilisation d'un tableau sur la pile et le tas? Maintenant je me rends compte il y a un coût pour l'utilisation de malloc ou le nouvel opérateur, mais à un certain point (tout comme Big O analyse) lorsque l'ensemble de données devient importante, serait-on mieux une itération à travers un tableau dynamique pour éviter les lignes principales de l'indexation?
Quoi que vous fassiez, vous êtes encore en train de faire ligne principale (plutôt que la colonne principale). Essayez de la mesure.
Pile vs tas est une question dont la répartition est et quand vous savez comment il est grand, n'est pas une question de mise en page des données. Comme Grijesh Chauhan déjà dit, vous pouvez utiliser la même mise en page multi-dimensionnelle tableau statique sur le tas, vous n'avez pas obtenu sucre syntaxique pour elle. Vous pouvez également avoir un tableau statique de pointeurs de tableaux (et parfois on a de sens, bien que le fait de trop tableaux seront souvent allouée dynamiquement).
Si vous êtes juste de parcourir le tableau, un standard de l'optimisation de la technique dite de la "réduction de la résistance" permet aux multiplications à être convertis à des additions. Depuis plus de 40 ans. Bien que la multiplication est de moins en moins une préoccupation majeure de ces jours; les défauts de cache sont bien pire.
Une autre question intéressante serait de la performance d'un tableau statique mis en œuvre sur la pile par rapport à un tableau statique mis en œuvre dans une section de données.
OriginalL'auteur Paul Renton | 2013-07-21
Vous devez vous connecter pour publier un commentaire.
Ces "plaine" C (pas C++).
D'abord, disons-le clairement la terminologie
"statique" est un mot-clé dans C, ce qui va modifier radicalement la façon dont votre variable est allouée /accessible si elle est appliquée sur des variables déclarées à l'intérieur des fonctions.
Il y a 3 endroits (sujet C) lorsqu'une variable (y compris les tableaux) peut s'asseoir:
static
.static
ou pas, il y a le mot se rapporte à la visibilité), et de toute fonction de variables locales déclaréesstatic
.malloc()
&free()
) visé par un pointeur. Vous avez accès à ces données par le biais de pointeurs.Maintenant, nous allons voir comment on dimensions des tableaux sont accessibles
Si vous accédez à un tableau avec une constante de l'indice (peut être
#define
d, mais pasconst
dans la plaine, C), cet indice peut être calculé par le compilateur. Si vous avez un vrai tableau dans le section de Données, il sera accessible sans indirection. Si vous avez un pointeur (Tas) ou un tableau sur le Pile, une indirection est toujours nécessaire. Si les tableaux dans le section de Données avec ce type d'accès peut être un petit peu plus rapide. Mais ce n'est pas une chose très utile qui serait à son tour le monde.Si vous accédez à un tableau avec une variable d'index, pour l'essentiel, de toujours se désintègre à un pointeur, puisque l'indice de changement (par exemple incrémenter dans une boucle for). Le code généré sera probablement très similaires, voire identiques pour tous les types ici.
Apporter plus de dimensions
Si vous déclarez une deux ou plus de deux dimensions tableau, et l'accès, partiellement ou entièrement par des constantes, avec un savant compilateur peut bien optimiser ces constantes comme ci-dessus.
Si vous accédez par des indices, notez que la mémoire est linéaire. Si plus tard les dimensions d'un véritable tableau ne sont pas un multiple de 2, le compilateur devra générer des multiplications. Par exemple, dans le tableau,
int arr[4][12];
la deuxième dimension est de 12. Si vous avez maintenant accès commearr[i][j]
oùi
etj
d'index sont variables, la mémoire linéaire doit être indexé comme12 * i + j
. Ainsi, le compilateur doit générer du code à multiplier par une constante ici. La complexité dépend de la façon dont "loin" de la constante, c'est à partir d'une puissance de 2. Ici, le code résultant sera probablement ressembler à quelque chose comme le calcul de(i<<3) + (i<<2) + j
pour accéder à l'élément dans le tableau.Si vous construisez les deux dimensions "tableau" de pointeurs, la taille des dimensions n'a pas d'importance car il y a des pointeurs de référence dans votre structure. Ici, si vous pouvez écrire
arr[i][j]
, ce qui implique que vous avez déclaré comme par exempleint* arr[4]
, puismalloc()
ed quatre segments de mémoire de 12int
s chacune. Notez que votre quatre pointeurs (qui le compilateur peut utiliser comme base) aussi consommer de la mémoire qui n'était pas pris si c'était un vrai tableau. Notez aussi que ici le code généré contient une double indirection: d'Abord le code des charges d'un pointeur pari
dearr
, puis il va charger unint
à partir de ce pointeur parj
.Si les longueurs sont "loin" de puissances de 2 (donc complexe "se multiplient avec la constante" codes devraient être générés pour accéder aux éléments), puis en utilisant les pointeurs peuvent générer plus rapidement des codes d'accès.
Comme James Kanze mentionné dans sa réponse, dans certains cas, le compilateur peut être en mesure d'optimiser l'accès pour les multi-dimensions des tableaux. Ce type d'optimisation est impossible pour des tableaux composés de pointeurs comme le "tableau" est en réalité pas un linéaire de la partie de la mémoire de cette affaire.
Localité questions
Si vous développez pour habitude de bureau /mobile architectures (Intel /ARM 32 /64 bits des processeurs) localité aussi des questions. Qu'est-ce qui est probablement assis dans le cache. Si vos variables sont déjà dans le cache pour une raison quelconque, ils seront accessibles plus rapidement.
Dans la durée de la localité Pile est toujours le gagnant, depuis le Pile est si souvent utilisé, il est très probable pour toujours assis dans le cache. Donc, les petits tableaux sont mieux mis en.
À l'aide de multi-dimensions des tableaux au lieu de composer un de pointeurs peuvent aussi aider sur ce terrain depuis un vrai tableau est toujours linéaire d'une partie de la mémoire, de sorte qu'il est généralement ayez besoin de moins de blocs de cache à charge. Une dispersion du pointeur de la composition (c'est-si l'aide séparément
malloc()
ed morceaux) au contraire pourrait avoir besoin de plus de cache de blocs, et peut s'élever ligne de cache conflits en fonction de la façon dont les morceaux physiquement fini sur le tas.Je pense qu'il y a au moins un autre type de type de variable dans la section de données et qui est thread local de stockage (par exemple, avec
__thread int x
)Brillante réponse Jubatian. J'étais conscient de la section de données, parce que j'ai commencé la programmation en assembleur, mais je n'ai jamais compris comment il s'est impliqué avec le langage C. Je ne savais à propos de la pile et le tas. Donc, dans ce cas, seules les variables globales résider dans la section de données? Est la section de données plus grande que tas? Techniquement, mondiale en ce qui concerne la visibilité d'un tableau alloué dynamiquement, est aussi un "global" dans le tableau. Pouvez-vous élaborer un peu plus dans la section de données? J'aimerais en savoir plus. En tout cas étonnant de réponse. Je suis de la sauver.
La taille des sections dépendent de l'architecture et du compilateur, si vous êtes vraiment en elle, vous auriez plus de chances de les trouver dans un linker script. Habituellement, vous n'aurait pas à s'intéressent beaucoup à ce sujet que les données de l'allocation est déterminée au moment de la compilation (pour s'adapter à toutes vos variables globales), et le reste de la surface, sur un microcontrôleur est utilisé pour la pile et le tas (sur un ordinateur de bureau, vous n'ont généralement pas besoin d'y toucher). Donc, le plus souvent ce que vous obtenez par défaut doit être juste.
OriginalL'auteur Jubatian
De la manière habituelle de mise en œuvre d'un 2 dimensions tableau en C++
serait l'envelopper dans une classe, à l'aide de
std::vector<int>
, etont de la classe des accesseurs qui le calcul de l'indice. Cependant:
Toutes les questions concernant l'optimisation ne peut être répondu par
de mesure, et même alors, ils ne sont valables que pour le compilateur
que vous utilisez sur l'ordinateur sur lequel vous effectuez les mesures.
Si vous écrivez:
et puis quelque chose comme:
Il est difficile d'imaginer un compilateur, ce qui n'est pas réellement générer
quelque chose le long des lignes de:
C'est l'un des plus fondamentaux des optimisations de autour de, et
a été pour les moins de 30 ans.
Si vous allouer dynamiquement comme vous le proposer, le compilateur va
pas être en mesure d'appliquer cette optimisation. Et même pour une seule
accès: la matrice a les plus pauvres de la localité, et nécessite plus de mémoire
accès, risque d'être moins performant.
Si vous êtes en C++, vous le feriez normalement écrire un
Matrix
classe,à l'aide de
std::vector<int>
pour la mémoire, et le calcul de laindex explicitement à l'aide de la multiplication. (L'amélioration de la localité
aura sans doute pour conséquence de meilleures performances, en dépit de la
la multiplication.) Cela pourrait rendre plus difficile pour l'
compilateur de faire de l'optimisation ci-haut, mais si cela s'avère
être un problème, vous pouvez toujours fournir des itérateurs pour
la manipulation de ce un cas particulier. Vous vous retrouvez avec plus de
lisible et plus souple de code (par exemple, les dimensions n'ont pas
être constant), à peu ou pas de perte de performance.
Pour la boucle d'optimisation est viable, mais dans de nombreux cas, il peut ne pas être possible d'effectuer. Par exemple, si les indices sont utilisés dans le code, ou probablement même les boucles elles-mêmes peuvent avoir d'autres résiliation anticipée points. Habituellement, un multi dim tableau est utilisé car il est plus propre à exprimer un algorithme par l'algorithme sera bien évidemment l'utilisation des indices d'une certaine manière. Eh bien, je ne suis pas beaucoup plus dans les compilateurs (j'ai l'habitude de travailler avec 8bit intégrées de l'uc qui sont plus simples compilateurs), c'est peut-être possible pour plus d'ampleur que ce que je crois.
Je ne sais pas à propos d'un nom, mais c'était une commune de l'optimisation de retour dans les années 1970, je serais surpris que n'importe quel compilateur aujourd'hui ne l'a pas fait.
OriginalL'auteur James Kanze
Quant à l'option qui offre de meilleures performances, alors la réponse dépendra en grande partie de votre situation spécifique. La seule façon de savoir si une solution est meilleure ou s'ils sont à peu près équivalent est de mesurer les performances de votre application.
Certaines choses qui seraient un facteur sont: combien de fois vous le faites, la taille réelle de l'tableaux/de données, la quantité de mémoire de votre système, et comment votre système qui gère la mémoire.
Si vous avez le luxe de pouvoir choisir entre deux choix, il doit signifier que les tailles sont déjà cloués. Ensuite, vous n'avez pas besoin de plusieurs schémas d'allocation que vous avez illustré. Vous pouvez effectuer une seule allocation dynamique de votre tableau 2D. Dans C:
En C++:
Tant que le
COLUMNS
est cloué vers le bas, vous pouvez effectuer une allocation unique pour obtenir votre tableau 2D. Si aucun n'est trouvé, alors vous n'avez pas vraiment le choix de l'utilisation d'un tableau statique de toute façon.Matrix
classe, qui peut utiliserstd::vector<std::array<int, COLUMNS>>
oustd::vector<int>
(et la multiplication dans l'index des opérateurs), selon ce qui est plus rapide sur votre machine. (En général, si mon expérience est quelque chose aller près, c'est le dernier.)Sur plus d'égard, ils ne sont pas aussi différentes que moi si. Je pensais
std::vector<std::vector<>>
.OriginalL'auteur jxh
Il y a souvent un compromis entre la consommation de la mémoire et de la vitesse. Empiriquement, j'ai constaté que la création de tableau sur la pile est plus rapide que l'allocation sur le tas. Comme la taille de la matrice augmente cela devient plus évident.
Vous pouvez toujours diminuer la consommation de mémoire. Par exemple, vous pouvez utiliser short ou char au lieu de int, etc.
Que la taille de la matrice augmente, en particulier avec l'utilisation de realloc, il pourrait y avoir beaucoup plus de remplacement de page (haut et bas) pour maintenir l'emplacement contigu d'éléments.
Vous devriez également considérer qu'il existe une limite inférieure de la taille des choses que vous pouvez stocker dans la pile, pour des tas de cette limite est plus élevée, mais comme je l'ai dit avec le coût de l'exécution.
Il n'y a pas nécessairement une différence dans la performance. Cela dépend de ce que vous faites, et comment le compilateur gère.
vous avez raison à propos de limite d'empilement pour les tableaux , James. J'ai été éditer ma réponse en conséquence.
OriginalL'auteur sgun
Tige d'allocation de mémoire offre un accès plus rapide des données que le Tas. Le CPU serait à la recherche de l'adresse dans le cache si il ne l'a pas, si il ne trouve pas l'adresse dans le cache alors il faudrait chercher dans la mémoire principale. La tige est un lieu privilégié après la cache.
OriginalL'auteur Juniar