31 votes

Pourquoi des tableaux de tailles entières différentes ont-ils des performances différentes?

J'ai des problème suivant:

Le temps d'écriture à un std::array pour int8, int16, int32 et int64 sont en doublant à chaque augmentation de la taille. Je peux comprendre ce genre de comportement pour un 8-bit CPU, mais pas 32/64-bit.

Pourquoi un système 32 bits besoin de 4 fois plus de temps pour enregistrer des valeurs de 32 bits que pour économiser 8-bit valeurs?

Voici mon code de test:

#include <iostream>
#include <array>
#include <chrono>

std::array<std::int8_t, 64 * 1024 * 1024> int8Array;
std::array<std::int16_t, 64 * 1024 * 1024> int16Array;
std::array<std::int32_t, 64 * 1024 * 1024> int32Array;
std::array<std::int64_t, 64 * 1024 * 1024> int64Array;

void PutZero()
{
    auto point1 = std::chrono::high_resolution_clock::now();
    for (auto &v : int8Array) v = 0;
    auto point2 = std::chrono::high_resolution_clock::now();
    for (auto &v : int16Array) v = 0;
    auto point3 = std::chrono::high_resolution_clock::now();
    for (auto &v : int32Array) v = 0;
    auto point4 = std::chrono::high_resolution_clock::now();
    for (auto &v : int64Array) v = 0;
    auto point5 = std::chrono::high_resolution_clock::now();
    std::cout << "Time of processing int8 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point2 - point1)).count() << "us." << std::endl;
    std::cout << "Time of processing int16 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point3 - point2)).count() << "us." << std::endl;
    std::cout << "Time of processing int32 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point4 - point3)).count() << "us." << std::endl;
    std::cout << "Time of processing int64 array:\t" << (std::chrono::duration_cast<std::chrono::microseconds>(point5 - point4)).count() << "us." << std::endl;
}

int main()
{
    PutZero();
    std::cout << std::endl << "Press enter to exit" << std::endl;
    std::cin.get();
    return 0;
}

Je le compiler sous linux avec: g++ -o array_issue_1 main.cpp -O3 -std=c++14

et mes résultats sont les suivants:

Time of processing int8 array:  9922us.   
Time of processing int16 array: 37717us.   
Time of processing int32 array: 76064us.   
Time of processing int64 array: 146803us.   

Si je compile avec -O2, puis les résultats sont 5 fois plus pour int8!

Vous pouvez aussi compiler cette source sous Windows. Vous obtiendrez relation similaire entre les résultats.

Mise à jour #1

Quand je compile avec -O2, alors que mes résultats sont les suivants:

Time of processing int8 array:  60182us.  
Time of processing int16 array: 77807us.  
Time of processing int32 array: 114204us.  
Time of processing int64 array: 186664us.  

Je n'ai pas d'analyser l'assembleur de sortie. Mon point principal est que je voudrais écrire du code efficace en C++ et des choses comme qui montrent, que des choses comme std::array peut être difficile du point de vue des performances et d'une certaine manière contre-intuitive.

65voto

Mysticial Points 180300

Pourquoi un système 32 bits besoin de 4 fois plus de temps pour enregistrer des valeurs de 32 bits que pour économiser 8-bit valeurs?

Il n'a pas. Mais il existe 3 types de problèmes avec votre référence que vous donner ces résultats.

  1. Vous n'êtes pas pré-failles de la mémoire. Si vous êtes à la page des failles d'effondrement du cours des tableaux de référence. Ces défauts de page avec le noyau de système d'exploitation de l'interaction sont un facteur dominant dans le temps.
  2. Le compilateur -O3 est complètement à battre l'indice de référence de convertir tous vos boucles en memset().
  3. Votre indice de référence liés à la mémoire. Si vous êtes à la mesure de la vitesse de votre mémoire à la place du cœur.

Problème 1: Les Données de Test n'est pas Prefaulted

Vos tableaux sont déclarées, mais n'est pas utilisée avant l'indice de référence. En raison de la manière dont le noyau et l'allocation de la mémoire des œuvres, ils ne sont pas mappés en mémoire encore. C'est seulement lorsque vous les touchez cela se passe. Et quand il le fait, il contracte une très grande peine à partir du noyau de la carte de la page.

Cela peut être fait en touchant tous les tableaux avant de l'indice de référence.

Pas de Pré-Défaillant: http://coliru.stacked-crooked.com/a/1df1f3f9de420d18

g++ -O3 -Wall main.cpp && ./a.out
Time of processing int8 array:  28983us.
Time of processing int16 array: 57100us.
Time of processing int32 array: 113361us.
Time of processing int64 array: 224451us.

Avec Pré-Défaillant: http://coliru.stacked-crooked.com/a/7e62b9c7ca19c128

g++ -O3 -Wall main.cpp && ./a.out
Time of processing int8 array:  6216us.
Time of processing int16 array: 12472us.
Time of processing int32 array: 24961us.
Time of processing int64 array: 49886us.

Le temps de chute de près d'un facteur 4. En d'autres termes, l'original de référence a été de mesurer plus du noyau que le code réel.


Problème 2: Le Compilateur est de Vaincre l'indice de Référence

Le compilateur est reconnaissant de votre motif de l'écriture de zéros et de l'est de remplacer complètement tous vos boucles avec des appels à des memset(). Donc en effet, vous êtes en mesure d'appels d' memset() avec des tailles différentes.

  call std::chrono::_V2::system_clock::now()
  xor esi, esi
  mov edx, 67108864
  mov edi, OFFSET FLAT:int8Array
  mov r14, rax
  call memset
  call std::chrono::_V2::system_clock::now()
  xor esi, esi
  mov edx, 134217728
  mov edi, OFFSET FLAT:int16Array
  mov r13, rax
  call memset
  call std::chrono::_V2::system_clock::now()
  xor esi, esi
  mov edx, 268435456
  mov edi, OFFSET FLAT:int32Array
  mov r12, rax
  call memset
  call std::chrono::_V2::system_clock::now()
  xor esi, esi
  mov edx, 536870912
  mov edi, OFFSET FLAT:int64Array
  mov rbp, rax
  call memset
  call std::chrono::_V2::system_clock::now()

L'optimisation de la faire, c'est -ftree-loop-distribute-patterns. Même si vous désactiver cette fonction, le vectorizer vous donnera un effet similaire.


Avec -O2, la vectorisation et la reconnaissance des formes sont tous deux désactivés. Ainsi, le compilateur vous donne ce que vous écrivez.

.L4:
  mov BYTE PTR [rax], 0         ;; <<------ 1 byte at a time
  add rax, 1
  cmp rdx, rax
  jne .L4
  call std::chrono::_V2::system_clock::now()
  mov rbp, rax
  mov eax, OFFSET FLAT:int16Array
  lea rdx, [rax+134217728]
.L5:
  xor ecx, ecx
  add rax, 2
  mov WORD PTR [rax-2], cx      ;; <<------ 2 bytes at a time
  cmp rdx, rax
  jne .L5
  call std::chrono::_V2::system_clock::now()
  mov r12, rax
  mov eax, OFFSET FLAT:int32Array
  lea rdx, [rax+268435456]
.L6:
  mov DWORD PTR [rax], 0        ;; <<------ 4 bytes at a time
  add rax, 4
  cmp rax, rdx
  jne .L6
  call std::chrono::_V2::system_clock::now()
  mov r13, rax
  mov eax, OFFSET FLAT:int64Array
  lea rdx, [rax+536870912]
.L7:
  mov QWORD PTR [rax], 0        ;; <<------ 8 bytes at a time
  add rax, 8
  cmp rdx, rax
  jne .L7
  call std::chrono::_V2::system_clock::now()

Avec -O2: http://coliru.stacked-crooked.com/a/edfdfaaf7ec2882e

g++ -O2 -Wall main.cpp && ./a.out
Time of processing int8 array:  28414us.
Time of processing int16 array: 22617us.
Time of processing int32 array: 32551us.
Time of processing int64 array: 56591us.

Maintenant, il est clair que le plus petit mot tailles sont plus lents. Mais vous attendez à ce que le temps d'être à plat si tous le mot tailles ont la même vitesse. Et la raison pour laquelle ils ne sont pas est à cause de la bande passante mémoire.


Problème 3: Bande Passante De La Mémoire

Parce que l'indice de référence (comme l'écrit) est seulement écrit des zéros, il est facile de saturer la bande passante de la mémoire pour le core/système. Donc, la référence devient affectée par la quantité de mémoire est touché.

Pour remédier à cela, nous avons besoin pour réduire l'ensemble de données afin qu'il s'intègre dans le cache. Pour compenser cela, nous avons en boucle sur les mêmes données plusieurs fois.

std::array<std::int8_t, 512> int8Array;
std::array<std::int16_t, 512> int16Array;
std::array<std::int32_t, 512> int32Array;
std::array<std::int64_t, 512> int64Array;

...

auto point1 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int8Array) v = 0;
auto point2 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int16Array) v = 0;
auto point3 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int32Array) v = 0;
auto point4 = std::chrono::high_resolution_clock::now();
for (int c = 0; c < 64 * 1024; c++) for (auto &v : int64Array) v = 0;
auto point5 = std::chrono::high_resolution_clock::now();

Maintenant, nous voyons les délais qui sont beaucoup plus plats pour les différents mot-tailles:

http://coliru.stacked-crooked.com/a/f534f98f6d840c5c

g++ -O2 -Wall main.cpp && ./a.out
Time of processing int8 array:  20487us.
Time of processing int16 array: 21965us.
Time of processing int32 array: 32569us.
Time of processing int64 array: 26059us.

La raison pourquoi il n'est pas complètement à plat est probablement parce qu'il y a de nombreux autres facteurs impliqués avec les optimisations du compilateur. Vous pourriez avoir besoin de recourir à la boucle-déroulage de s'en approcher.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X