7
votes

Pourquoi le vecteur (taille) est plus lent que neuf []?

J'étais analyser des algorithmes stl et j'ai été surpris par le temps pris par le code suivant: (J'ai mesuré le code compilé g ++ [aucune optimisation] avec la commande la commande ) xxx

Le temps passé par une initialisation de vecteur est 2.26S tandis qu'un Nouveau (et Supprimer ) prend 1,29. Que fait le vecteur Ctor qui prendrait tellement de plus de temps? nouveau [] appelle le constructeur sur chaque élément, tout comme le vecteur cors, non?

J'ai ensuite compilé avec -O3, il est allé tous Plus rapide, mais il y avait toujours un écart entre les deux codes. (J'ai obtenu respectivement 0,83 et 0,75s)

Des idées?


7 commentaires

En plus de ce que DIAMDMG a dit, cela n'a aucun sens de faire des repères «un tir» (et, comme vous l'avez vu, de repères sans optimisations), car il existe des fluctuations statistiques en raison de cache Misses & Co. Vous devez répéter l'indice de référence un nombre important de fois (dans le même processus), mesurer l'heure de chaque test et calculer la moyenne et l'écart type. Ce n'est qu'après avoir reçu ces données, vous pouvez faire une comparaison raisonnable: si les deux fois diffèrent de moins que l'incertitude de votre mesure (calculé en combinant les deux écarts types en RSS), il n'y a pas de différence significative.


Notez que, puisque votre code fait uniquement référence onglet [0] , il est parfaitement légal pour le compilateur d'allouer à ce seul élément. En fait, étant donné que vous écrivez uniquement dans onglet [0] et ne lisez jamais la valeur en arrière, et ce n'est pas volatile , il est parfaitement légal pour le compilateur pour retirer complètement le corps. de principal () dans la sortie, car il n'a aucun effet secondaire observable.


J'ai essayé ce test dans Visual Studio, obtenez les résultats opposés (meilleur moment pour le vecteur d'environ 10% de mieux), mais le bruit de mesure (~ 20-25%) était plus grand que la différence. Donc, ces mesures sont vraiment discutées lorsqu'il s'agit d'applications réelles.


"J'étais benchmarking ... pas d'optimisations" alors c'est entièrement inutile. C'est comme demander laquelle de deux voitures a une vitesse maximale plus élevée ... tout en les forçant à rester neutre.


@Gman: Il y a en effet une utilisation dans l'analyse comparative du code non optimisé: lorsque je déboque mon programme, le code n'est pas optimisé, et il est toujours bon de déboguer plus rapidement. Mais je conviens que le plus pertinent est du code optimisé du banc.


@PAVEL: Je suis conscient que le compilateur pourrait supprimer le code usound avec des optims activés, mais ici, la mesure du temps montre clairement qu'il fait encore quelque chose.


@Matteo: Je vous ai donné une moyenne d'exécutant de 20 tirs, j'ai oublié de le mentionner.


3 Réponses :


9
votes

La vitesse dépendra de la mise en œuvre, mais la plupart des raisons probablement du vecteur étant plus lente sont que le vecteur ne peut pas construire par défaut ses éléments. Les éléments de vecteur sont toujours en copie. Par exemple xxx pré>

est interprété comme p> xxx pré>

i.e. Le deuxième argument obtient sa valeur de l'argument par défaut. Le vecteur attribue ensuite la mémoire brute et les copies em> cet élément construit par défaut passait de l'extérieur dans chaque élément du nouveau vecteur (en utilisant Copy-Constructor). Cela pourrait être généralement plus lent que la construction par défaut de chaque élément directement (comme nouveau [] code> fait). P>

Pour illustrer la différence avec un crochet de code, nouveau Vec2 [taille ] code> est à peu près équivalent à p> xxx pré>

tandis que vecteur (taille) code> est à peu près équivalent à p>

vec2 source; // Default-constructed "original" element

vec2 *v = (vec2 *) malloc(size * sizeof(vec2));

for (size_t i = 0; i < size; ++i)
  // Copy-construct `v[i]` in place
  new (&v[i]) vec2(source);

return v;


2 commentaires

Je parie que le compilateur a lancé le constructeur par défaut, éventuellement une enveloppe spéciale le "bloc de mémoire initialise à zéro" opération.


Il serait peut-être bon de noter que dans C ++ 0x, appeler le constructeur d'argument de vecteur par défaut - initialise les éléments qu'il crée, au lieu de copier le "modèle".



7
votes

Les deux versions initialisent la mémoire.

Comme plusieurs personnes ont signalé, le vecteur utilise la construction de la copie pendant que le tableau utilise le constructeur par défaut. Votre compilateur semble optimiser ce dernier mieux que celui des anciens.

Notez que dans la vie réelle, vous souhaitez rarement à initialiser un tel éventail énorme dans un coup de foudre. (Quelle utilisation est une bande de zéros? Évidemment, vous avez l'intention de mettre autre chose à partir d'habitude ... et d'initialiser des centaines de mégaoctets est très désagréable.)

à la place, vous écririez quelque chose comme: < / p> xxx

puis lorsque vous êtes prêt à mettre un élément réel dans le vecteur, vous utilisez v.push_back (élément) . La réserve () attribue la mémoire sans l'initialisation; Le push_back () Copie-construit dans l'espace réservé.

Alternativement, lorsque vous souhaitez mettre un nouvel élément dans le vecteur, vous pouvez utiliser v.résize ( v.Size () + 1) puis modifier l'élément v.back () . (Voici comment un "allocator de la piscine" pourrait fonctionner.) Bien que cette séquence initialisera l'élément, puis écrasez-la, elle se produira tous dans le cache L1, qui est presque aussi rapide que de ne pas l'initialiser du tout.

Donc, pour une comparaison équitable, essayez un grand vecteur (avec réserve ) contre un tableau pour créer une séquence d'éléments non identiques. Vous devriez trouver le vecteur est plus rapide.


0 commentaires

2
votes

Après analyse de l'assemblage généré par VC ++ pour ces deux cas, voici ce que j'ai trouvé. Le compilateur a inlisé pratiquement tout et a généré des boucles très similaires pour l'initialisation après une allocation de mémoire. En cas de boucle intérieure de vecteur ressemble à ceci: xxx

edx et eSi Les registres ont été zérod à l'extérieur de la boucle: xxx

en cas de Nouveau [] boucle interne ressemble à ceci: xxx

Les différences sont très insignifiantes , quelques instructions supplémentaires en cas de vecteur , mais probablement aussi plus rapide MOV S des registres. Depuis dans la plupart des cas de vie réelle, les constructeurs font beaucoup plus que d'attribuer des zéros, cette différence peut difficilement être perceptible du tout. De sorte que la valeur de ce test est discutable.


2 commentaires

Intéressant; Merci pour cela. Le premier code est évidemment inefficace; Le Jne retourne au test + Je qui ne peut pas éventuellement évaluer à vrai ... donc cela aurait pu faire le jne < / Code> Boucle retour au premier MOV . Aussi, êtes-vous sûr que le mouvement d'une constante zéro est plus rapide qu'un registre?


@NEMO: Je ne sais pas si la performance MOV est facile à analyser sur les processeurs modernes. Mais en vérifiant mon ancien livre Intel, 386 a fait MOV Mem, Reg et MOV MEM, PETIT en 2 cycles.