J'ai observé sur un système qui std::fill
sur un grand std::vector<int>
était significativement et toujours plus lente lors de la définition d'une valeur constante 0
par rapport à une valeur constante 1
ou une valeur dynamique:Pourquoi std :: fill (0) est-il plus lent que std :: fill (1)?
5,8 Gio/s vs 7,5 Gio/s
Cependant, les résultats sont différents pour les petites tailles de données, où fill(0)
est plus rapide:
avec plus d'un fil, à 4 la taille des données GiB, fill(1)
montre une pente plus élevée, mais atteint une beaucoup plus faible pic de fill(0)
(51 GiB/s vs 90 GiB/s):
Cette soulève la question secondaire, pourquoi la bande passante maximale de fill(1)
est tellement inférieure.
Le système de test pour cela était un CPU Intel Xeon double socket E5-2680 v3 réglé à 2,5 GHz (via /sys/cpufreq
) avec 8x16 GiB DDR4-2133. J'ai testé avec GCC 6.1.0 (-O3
) et le compilateur Intel 17.0.1 (-fast
), les deux obtiennent des résultats identiques. GOMP_CPU_AFFINITY=0,12,1,13,2,14,3,15,4,16,5,17,6,18,7,19,8,20,9,21,10,22,11,23
a été défini. Strem/add/24 threads obtient 85 GiB/s sur le système.
J'ai été capable de reproduire cet effet sur un autre système de serveur Haswell à double socket, mais pas sur n'importe quelle autre architecture. Par exemple sur Sandy Bridge EP, les performances de la mémoire sont identiques, tandis que dans le cache fill(0)
est beaucoup plus rapide.
Voici le code à reproduire:
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <omp.h>
#include <vector>
using value = int;
using vector = std::vector<value>;
constexpr size_t write_size = 8ll * 1024 * 1024 * 1024;
constexpr size_t max_data_size = 4ll * 1024 * 1024 * 1024;
void __attribute__((noinline)) fill0(vector& v) {
std::fill(v.begin(), v.end(), 0);
}
void __attribute__((noinline)) fill1(vector& v) {
std::fill(v.begin(), v.end(), 1);
}
void bench(size_t data_size, int nthreads) {
#pragma omp parallel num_threads(nthreads)
{
vector v(data_size/(sizeof(value) * nthreads));
auto repeat = write_size/data_size;
#pragma omp barrier
auto t0 = omp_get_wtime();
for (auto r = 0; r < repeat; r++)
fill0(v);
#pragma omp barrier
auto t1 = omp_get_wtime();
for (auto r = 0; r < repeat; r++)
fill1(v);
#pragma omp barrier
auto t2 = omp_get_wtime();
#pragma omp master
std::cout << data_size << ", " << nthreads << ", " << write_size/(t1 - t0) << ", "
<< write_size/(t2 - t1) << "\n";
}
}
int main(int argc, const char* argv[]) {
std::cout << "size,nthreads,fill0,fill1\n";
for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
bench(bytes, 1);
}
for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
bench(bytes, omp_get_max_threads());
}
for (int nthreads = 1; nthreads <= omp_get_max_threads(); nthreads++) {
bench(max_data_size, nthreads);
}
}
Les résultats présentés compilé avec g++ fillbench.cpp -O3 -o fillbench_gcc -fopenmp
.
Quelle est la taille des données lorsque vous comparez le nombre de threads? –
@GavinPortwood 4 GiB, donc en mémoire, pas de cache. – Zulan
Ensuite, il doit y avoir quelque chose de mal avec la deuxième intrigue, la mise à l'échelle faible. Je ne peux pas imaginer qu'il faudrait plus de deux threads pour saturer la bande passante de la mémoire pour une boucle avec des opérations intermédiaires minimes. En fait, vous n'avez pas identifié le nombre de threads où la bande passante sature même à 24 threads. Pouvez-vous montrer qu'il se stabilise à un certain nombre de threads finis? –