push_back
, как ни странно, немного перегружены, потому что он на самом деле не знает, что вы зарезервировали достаточно места, поэтому всегда должен проверять. Поскольку эта проверка может изменить поток управления между l oop итерациями, push_back
исключает автоматическую c векторизацию компилятором.
Рассмотрим эти две функции, где первая использует push_back
, а вторая один изменяет копию (или перемещенное значение) на месте:
auto exp1(std::vector<double> const& xs) -> std::vector<double> {
auto ys = std::vector<double>{};
ys.reserve(xs.size());
for(auto x : xs){ ys.push_back(std::exp(x)); }
}
auto exp2(std::vector<double> xs) -> std::vector<double> {
for(auto & x : xs){ x = std::exp(x); }
return xs;
}
Мы рассмотрим вывод сборки , если он скомпилирован в G CC 9.1 с
gcc -std=c++17 -O3 -march=skylake-avx512
Вот внутренний exp1
l oop (встроенный в довольно много дополнительного кода, который никогда не будет выполнен, потому что вы уже reserve
d):
.L45:
add rbx, 8
vmovsd QWORD PTR [r14], xmm0
add r14, 8
cmp r12, rbx
je .L44
.L18:
vmovsd xmm0, QWORD PTR [rbx]
call exp
vmovsd QWORD PTR [rsp], xmm0
cmp rbp, r14
jne .L45
А вот exp2
:
.L53:
vmovsd xmm0, QWORD PTR [rbx]
add rbx, 8
call exp
vmovsd QWORD PTR [rbx-8], xmm0
cmp rbp, rbx
jne .L53
На практике они в основном одинаковы, потому что exp
сложный и G CC не знает, как автоматически его векторизовать. Однако рассмотрим случай, когда во внутреннем l oop происходит нечто гораздо более простое:
auto sq1(std::vector<double> const& xs) -> std::vector<double> {
auto ys = std::vector<double>{};
ys.reserve(xs.size());
for(auto x : xs){ ys.push_back(x*x); }
}
auto sq2(std::vector<double> xs) -> std::vector<double> {
for(auto & x : xs){ x *= x; }
return xs;
}
Вот внутреннее l oop sq1
:
.L89:
vmovsd QWORD PTR [rsi], xmm0
add rbx, 8
add rsi, 8
mov QWORD PTR [rsp+24], rsi
cmp rbp, rbx
je .L72
.L75:
vmovsd xmm0, QWORD PTR [rbx]
mov rsi, QWORD PTR [rsp+24]
vmulsd xmm0, xmm0, xmm0
vmovsd QWORD PTR [rsp+8], xmm0
cmp rsi, QWORD PTR [rsp+32]
jne .L89
Вот sq2
's. Обратите внимание, что он использует регистры vmulpd
и ymm
и что он скачет на 32 байта за раз, а не на 8 за один раз.
.L11:
vmovupd ymm0, YMMWORD PTR [rdx]
add rdx, 32
vmulpd ymm0, ymm0, ymm0
vmovupd YMMWORD PTR [rdx-32], ymm0
cmp rdx, rcx
jne .L11
Конечно, этот внутренний фрагмент l oop немного вводит в заблуждение: он скрывает огромное количество кода, используемого для обработки оставшейся части std::vector
, если его размер не делится равномерно на 4. Тем не менее, моя главная мысль в том, что да, вы на самом деле можете сделать немного лучше, чем reserve
+ push_back
(это меня немного удивило, когда я впервые узнал), и что было бы значительно лучше, если бы мы не имели дело с exp
в частности.