Правила псевдонимов C ++ не позволяют компилятору доказать, что glob[i] += stuff
не изменяет один из элементов const vec v1 {1.0,-1.0,1.0};
или v2
.
const
onstd::vector
означает, что указатели «управляющего блока» могут считаться не измененными после его создания, но память по-прежнему распределяется динамически, и все, что знает компилятор, - это то, что он фактически имеет const double *
в статическом хранилище.
Ничто в реализации std::vector
не позволяет компилятору исключать какой-либо другой non-const
указатель, указывающий на это хранилище.Например, double *data
в блоке управления glob
.
C ++ не позволяет разработчикам библиотек предоставлять компилятору информацию о том, что хранилище для разных std::vector
sне перекрывается. Они не могут использовать __restrict
(даже на компиляторах, которые поддерживают это расширение), потому что это может нарушить работу программ, которые принимают адрес векторного элемента.См. документацию C99 для restrict
.
Но с const arr a1 {1.0,-1.0,1.0};
и a2
сами двойники могут помещаться в статическое хранилище только для чтения, и компилятор знает,этот. Поэтому он может оценивать comb(a1[0],a2[0]);
и т. Д. во время компиляции .В ответе @ Xirema вы можете видеть константы нагрузки asm .LC1
и .LC2
.(Только две константы, потому что оба a1[0]+a2[0]
и a1[2]+a2[2]
равны 1.0+1.0
. Тело цикла использует xmm2
в качестве операнда-источника для addsd
дважды, а другую константу - один раз.)
Но не мог ли компилятор по-прежнему делать суммы один раз за пределами цикла во время выполнения?
Нет, опять же из-за возможного алиасинга.Он не знает, что хранилища в glob[i+0..3]
не изменят содержимое v1[0..2]
, поэтому он перезагружается из v1 и v2 каждый раз через цикл после сохранения в glob
.
(Этооднако не нужно перезагружать указатели блока управления vector<>
, поскольку строгие правила псевдонимов на основе типов позволяют предположить, что сохранение double
не изменяет double*
.)
Компилятор мог бы проверить, что glob.data() + 0 .. N-3
не перекрывается ни с одним из v1/v1.data() + 0 .. 2
, и создать другую версию цикла для этого случая, выведя три comb()
результата из цикла.
Это полезная оптимизация, которую делают некоторые компиляторы при автоматической векторизации, если они не могут доказать отсутствие псевдонимов ;в вашем случае это явно пропущенная оптимизация, так как gcc не проверяет перекрытие, потому что это заставит функцию работать намного быстрее.Но вопрос заключается в том, может ли компилятор разумно догадаться, что стоило испускать asm, который проверяет во время выполнения на перекрытие и имеет 2 разные версии одного и того же цикла.При оптимизации по профилю он знал бы, что цикл горячий (работает много итераций), и на него стоит потратить дополнительное время.Но без этого компилятор, возможно, не захочет рисковать слишком большим количеством кода.
ICC19 (компилятор Intel) на самом деле делает что-то подобное здесь, но это странно: если вы посмотритев начале assemble_vec
( в проводнике компилятора Godbolt ) он загружает указатель данных из glob
, затем добавляет 8 и снова вычитает указатель, создавая константу 8
.Затем оно ветвится во время выполнения на 8 > 784
(не занято), а затем -8 < 784
(занято).Похоже, это должна была быть проверка на перекрытие, но, возможно, использовался один и тот же указатель дважды вместо v1 и v2?(784 = 8*100 - 16 = sizeof(double)*N - 16
)
В любом случае, он завершает выполнение цикла ..B2.19
, который поднимает все вычисления 3 comb()
, и, что интересно, выполняет 2 итерации сразу с 4 скалярными нагрузками и сохраняет в glob[i+0..4]
и 6 addsd
(скалярное двойное) добавить инструкции.
В другом месте в теле функции есть векторизованная версия, которая использует 3x addpd
(упакованный двойной), просто сохраняя / перезагружая 128-битные векторы, которыечастично перекрываютсяЭто приведет к остановке пересылки из хранилища, но выполнение по порядку может скрыть это.Просто очень странно, что он выполняет ветвление во время выполнения вычисления, которое будет каждый раз давать один и тот же результат, и никогда не использует этот цикл.Пахнет жуком.
Если бы glob[]
был статическим массивом , у вас все равно была бы проблема.Поскольку компилятор не может знать, что v1/v2.data()
не указывает на этот статический массив.
Я думал, если бы вы обращались к нему через double *__restrict g = &glob[0];
, не было быпроблема вообще.Это обещает компилятору, что g[i] += ...
не повлияет на любые значения, к которым вы обращаетесь через другие указатели, например v1[0]
.
На практике это не включает подъем comb()
для gcc, clang или ICC -O3
.Но это делает для MSVC.(Я читал, что MSVC не выполняет строгую оптимизацию псевдонимов на основе типов, но он не перезагружает glob.data()
внутри цикла, поэтому он каким-то образом выяснил, что сохранение double не изменит указатель. Но MSVC действительно определяетПоведение *(int*)my_float
для определения типа, в отличие от других реализаций C ++.)
Для тестирования Я положил это на Godbolt
//__attribute__((noinline))
void assemble_vec()
{
double *__restrict g = &glob[0]; // Helps MSVC, but not gcc/clang/ICC
// std::vector<double> &g = glob; // actually hurts ICC it seems?
// #define g glob // so use this as the alternative to __restrict
for (size_t i=0; i<N-2; ++i)
{
g[i] += comb(v1[0],v2[0]);
g[i+1] += comb(v1[1],v2[1]);
g[i+2] += comb(v1[2],v2[2]);
}
}
Мы получаем это от MSVCвне цикла
movsd xmm2, QWORD PTR [rcx] # v2[0]
movsd xmm3, QWORD PTR [rcx+8]
movsd xmm4, QWORD PTR [rcx+16]
addsd xmm2, QWORD PTR [rax] # += v1[0]
addsd xmm3, QWORD PTR [rax+8]
addsd xmm4, QWORD PTR [rax+16]
mov eax, 98 ; 00000062H
Тогда мы получим эффектно выглядящий цикл.
Так что это пропущенная оптимизация для gcc / clang / ICC.