Spadek wydajności spowodowany domyślną inicjalizacją elementów w standardowych kontenerach

(Tak, wiem, że jestpytanie o prawie tym samym tytule, ale odpowiedź nie była zadowalająca, patrz poniżej)

EDYTUJ Niestety, oryginalne pytanie nie używało optymalizacji kompilatora. Jest to naprawione, ale aby uniknąć trywialnej optymalizacji i zbliżyć się do mojego rzeczywistego przypadku użycia, test został podzielony na dwie jednostki kompilacji.

Fakt, że konstruktorstd::vector<> ma liniową złożoność, co jest uciążliwe w przypadku aplikacji o krytycznym znaczeniu dla wydajności. Rozważ ten prosty kod

// compilation unit 1:
void set_v0(type*x, size_t n)
{
  for(size_t i=0; i<n; ++i)
    x[i] = simple_function(i);
}

// compilation unit 2:
std::vector<type> x(n);                     // default initialisation is wasteful
set_v0(x.data(),n);                         // over-writes initial values

kiedy konstruuje się znaczną ilość czasux. Konwencjonalny sposób obejścia tego, jak zbadano przezto pytaniewydaje się, że po prostu zastrzega sobie przechowywanie i użytkowaniepush_back() wypełnić dane:

// compilation unit 1:
void set_v1(std::vector<type>&x, size_t n)
{
  x.reserve(n);
  for(size_t i=0; i<n; ++i)
    x.push_back(simple_function(i));
}

// compilation unit 2:
std::vector<type> x(); x.reserve(n);        // no initialisation
set_v1(x,n);                                // using push_back()

Jednak, jak wskazuje mój komentarz,push_back() jest z natury powolny, co czyni to drugie podejściewolniej niż pierwszy dla wystarczająco prostych obiektów, takich jaksize_ts, kiedy dla

simple_function = [](size_t i) { return i; };

Dostaję następujące czasy (używając gcc 4.8 z -O3; wygenerowano clang 3.2 ~ 10% wolniejszego kodu)

timing vector::vector(n) + set_v0();
n=10000 time: 3.9e-05 sec
n=100000 time: 0.00037 sec
n=1000000 time: 0.003678 sec
n=10000000 time: 0.03565 sec
n=100000000 time: 0.373275 sec

timing vector::vector() + vector::reserve(n) + set_v1();
n=10000 time: 1.9e-05 sec
n=100000 time: 0.00018 sec
n=1000000 time: 0.00177 sec
n=10000000 time: 0.020829 sec
n=100000000 time: 0.435393 sec

Przyspieszenie faktycznie możliwe, gdyby można było uniknąć domyślnej konstrukcji elementów, można oszacować za pomocą następującej wersji oszustwa

// compilation unit 2
std::vector<type> x; x.reserve(n);          // no initialisation
set_v0(x,n);                                // error: write beyond end of vector
                                            // note: vector::size() == 0

kiedy dostaniemy

timing vector::vector + vector::reserve(n) + set_v0();          (CHEATING)
n=10000 time: 8e-06 sec
n=100000 time: 7.2e-05 sec
n=1000000 time: 0.000776 sec
n=10000000 time: 0.01119 sec
n=100000000 time: 0.298024 sec

Więc mojapierwsze pytanie: Czy istnieje jakikolwiek legalny sposób użycia standardowego kontenera biblioteki, który dałby te ostatnie czasy? A może muszę uciekać się do zarządzania pamięcią?

Teraz naprawdę chcę używaćwielowątkowość wypełnić pojemnik. Naiwny kod (wykorzystujący openMP w tym przykładzie dla uproszczenia, który wyklucza na razie klang)

// compilation unit 1
void set_v0(type*x, size_t n)
{
#pragma omp for                       // only difference to set_v0() from above 
  for(size_t i=0; i<n; ++i)
    x[i] = simple_function(i);
}

// compilation unit 2:
std::vector<type> x(n);               // default initialisation not mutli-threaded
#pragma omp parallel
set_v0(x,n);                          // over-writes initial values in parallel

teraz cierpi z powodu faktu, że domyślna inicjalizacja wszystkich elementów nie jest wielowątkowa, co powoduje potencjalnie poważny spadek wydajności. Oto terminyset_omp_v0() i równoważna metoda oszustwa (przy użyciu mojego chipa intel i7 z 4 rdzeniami, 8 hiperwątków):

timing std::vector::vector(n) + omp parallel set_v0()
n=10000 time: 0.000389 sec
n=100000 time: 0.000226 sec
n=1000000 time: 0.001406 sec
n=10000000 time: 0.019833 sec
n=100000000 time: 0.35531 sec

timing vector::vector + vector::reserve(n) + omp parallel set_v0(); (CHEATING)
n=10000 time: 0.000222 sec
n=100000 time: 0.000243 sec
n=1000000 time: 0.000793 sec
n=10000000 time: 0.008952 sec
n=100000000 time: 0.089619 sec

Zauważ, że wersja oszustwa jest ~ 3.3 razy szybsza niż seryjna wersja oszustwa, mniej więcej zgodnie z oczekiwaniami, ale standardowa wersja nie jest.

Więc mojaDrugie Pytanie: Czy istnieje jakikolwiek legalny sposób użycia standardowego kontenera biblioteki, który dawałby te ostatnie czasy w sytuacjach wielowątkowych?

PS. znalazłemto pytanie, gdziestd::vector jest oszukany, aby uniknąć domyślnej inicjalizacji, dostarczając jejuninitialized_allocator. Nie jest to już zgodne ze standardem, ale działa bardzo dobrze w moim przypadku testowym (patrz moja odpowiedź poniżej ito pytanie dla szczegółów).

questionAnswers(4)

yourAnswerToTheQuestion