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_t
s, 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).