Comments 22
имхо, подобные детали лучше вынести из зоны ответственности разработчика прикладного софта в зону ответственности разработчика компиляторов, а точнее оптимизаторов.
Есть компиляторы которые не перестают удивлять rsdn.org/forum/cpp/6722631.1
… компилятор способный по не объяснимым причинам замедлить исполнение куска кода более чем в 100 раз...
Я бы сказал «по непредсказуемым причинам», а не «по необьяснимым».
Просто надо понимать, что оптимизирующий компилятор — это, всё-таки, немножко чёрной магии.
Вот родственный пример для GCC:
Переход от -O1 к -O3 = замедление, причём заметно большее, чем описываемое в статье!
Если взглянуть на сгенерированный код — то сразу понятно, откуда замедление, но совершенно непонятно, как переход от -O1 к -O3 его провоцировал…
Просто в GCC производительностью занились много лет назад и замедление в 100 раз всегда считалось чем-то ужасным, так что такие катастрофы, как в MSVC уже подвыловлены. А разработчики MSVC занялись производительностью не так и давно, так что…
Просто надо понимать, что оптимизирующий компилятор — это, всё-таки, немножко чёрной магии.
Вот родственный пример для GCC:
$ gcc -O1 test2.cc test1.cc -o test $ time ./test 0m51.66s real 0m51.47s user 0m00.02s system $ gcc -O3 test2.cc test1.cc -o test $ time ./test 1m11.64s real 1m11.30s user 0m00.04s system $ cat /proc/cpuinfo | grep name model name : Intel(R) Atom(TM) CPU Z3560 @ 1.00GHz model name : Intel(R) Atom(TM) CPU Z3560 @ 1.00GHz model name : Intel(R) Atom(TM) CPU Z3560 @ 1.00GHz model name : Intel(R) Atom(TM) CPU Z3560 @ 1.00GHz
Переход от -O1 к -O3 = замедление, причём заметно большее, чем описываемое в статье!
Исходники
test.h:
test1.c:
test2.c:
#include <inttypes.h>
struct pair {
uint64_t low;
uint64_t hi;
};
pair add(pair& a, pair& b);
test1.c:
#include "test.h"
pair add(pair& a, pair& b) {
pair s;
s.low = a.low + b.low;
s.hi = a.hi + b.hi + (s.low < a.low); //carry
return s;
}
test2.c:
#include <stdio.h>
#include "test.h"
int main() {
pair a = { 0x4243444546474849, 0x4243444546474849 };
pair b = { 0x5758595a5b5c5d5e, 0x5758595a5b5c5d5e };
for (uint64_t i=0;i<10000000000;++i) {
a = add(a, b);
}
}
Если взглянуть на сгенерированный код — то сразу понятно, откуда замедление, но совершенно непонятно, как переход от -O1 к -O3 его провоцировал…
Просто в GCC производительностью занились много лет назад и замедление в 100 раз всегда считалось чем-то ужасным, так что такие катастрофы, как в MSVC уже подвыловлены. А разработчики MSVC занялись производительностью не так и давно, так что…
>> но совершенно непонятно, как переход от -O1 к -O3 его провоцировал
Лечится ключом -fno-expensive-optimizations
Какой-то проход видимо посчитал, что вероятность (s.low >= a.low) крайне низка.
Правильно предсказанный бранч занимает 0 тактов, а adc на интел целых 2.
Это можно увидеть используя
pair add(pair& a, pair& b) __attribute__((hot));
pair add(pair& a, pair& b) __attribute__((cold));
Во втором случае он перемещает mov ecx, 1 прямо за переход, не оптимизируя fetch, так сказать.
Кроме того, лишь x86 бэкенд генерит переход — на других процах такой фигни я не обнаружил.
Лечится ключом -fno-expensive-optimizations
Какой-то проход видимо посчитал, что вероятность (s.low >= a.low) крайне низка.
Правильно предсказанный бранч занимает 0 тактов, а adc на интел целых 2.
Это можно увидеть используя
pair add(pair& a, pair& b) __attribute__((hot));
pair add(pair& a, pair& b) __attribute__((cold));
Во втором случае он перемещает mov ecx, 1 прямо за переход, не оптимизируя fetch, так сказать.
Кроме того, лишь x86 бэкенд генерит переход — на других процах такой фигни я не обнаружил.
* вероятность (s.low >= a.low) крайне высока
Правильно предсказанный бранч занимает 0 тактов, а adc на интел целых 2.Однако
add
таки один такт занимает, а потому замена связки add+adc на add+jc+add+add — это в чистом виде пессимизация. Независимо от вероятностей код занимает либо 3 тракта, либо ещё больше. А в оригинале — он только 3 такта всегда занимал…>> add+jc+add+add
В этой связке первые две команды add не являются зависимыми (и они могут быть исполнены параллельно).
Цепочка зависимости это лишь последние add+add вместо add + adc.
На процах Интел до Skylake (IIRC), латентность ADC 2 такта.
_возможно_ этот такт и перевешивает. Я лишь предпогаю, как вы понимаете.
В этой связке первые две команды add не являются зависимыми (и они могут быть исполнены параллельно).
Цепочка зависимости это лишь последние add+add вместо add + adc.
На процах Интел до Skylake (IIRC), латентность ADC 2 такта.
_возможно_ этот такт и перевешивает. Я лишь предпогаю, как вы понимаете.
В этой связке первые две команды add не являются зависимыми (и они могут быть исполнены параллельно).Это если вы эту команду отдельно, не в цикле исполняете. Но там — это не так важно. А в цикле — вы займёте больше исполняемых устройств, задержка тут не так важна. Особенно если у вас иногда всё же предстказание переходов не срабатывает (в 25% случаев, ага), и вы получаете хорошую-такую задержку…
Короче всё намного проще.
Всё решается даже до преобразования в p-code.
При -O1 -fexpensive-optimizations срабатывает стадия 188t.widening_mul
(при обычном -O1 её нет)
godbolt.org/g/vDfnVb
Происходит преобразование
<bb 2> [100.00%]:
_1 = a_11(D)->low;
_2 = b_12(D)->low;
_3 = _1 + _2;
# DEBUG s$low => _3
_4 = a_11(D)->hi;
_5 = b_12(D)->hi;
_6 = _4 + _5;
_7 = _1 > _3;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
# DEBUG s$hi => _9
MEM[(struct pair *)&D.2410] = _3;
MEM[(struct pair *)&D.2410 + 8B] = _9;
вот в это
<bb 2> [100.00%]:
_1 = a_11(D)->low;
_2 = b_12(D)->low;
_15 = ADD_OVERFLOW (_1, _2);
_3 = REALPART_EXPR <_15>;
_16 = IMAGPART_EXPR <_15>;
# DEBUG s$low => _3
_4 = a_11(D)->hi;
_5 = b_12(D)->hi;
_6 = _4 + _5;
_7 = _16 != 0;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
# DEBUG s$hi => _9
MEM[(struct pair *)&D.2410] = _3;
MEM[(struct pair *)&D.2410 + 8B] = _9;
Имеем
_7 = _1 > _3;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
vs.
_7 = _16 != 0;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
не тут ли потерялся перенос?
Также если открыть 311t.statistics, то можно заметить что
266 combine «three-insn combine» «pair add(pair&, pair&)» 1
не применяется. Возможно это следствие.
Всё решается даже до преобразования в p-code.
При -O1 -fexpensive-optimizations срабатывает стадия 188t.widening_mul
(при обычном -O1 её нет)
godbolt.org/g/vDfnVb
Происходит преобразование
<bb 2> [100.00%]:
_1 = a_11(D)->low;
_2 = b_12(D)->low;
_3 = _1 + _2;
# DEBUG s$low => _3
_4 = a_11(D)->hi;
_5 = b_12(D)->hi;
_6 = _4 + _5;
_7 = _1 > _3;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
# DEBUG s$hi => _9
MEM[(struct pair *)&D.2410] = _3;
MEM[(struct pair *)&D.2410 + 8B] = _9;
вот в это
<bb 2> [100.00%]:
_1 = a_11(D)->low;
_2 = b_12(D)->low;
_15 = ADD_OVERFLOW (_1, _2);
_3 = REALPART_EXPR <_15>;
_16 = IMAGPART_EXPR <_15>;
# DEBUG s$low => _3
_4 = a_11(D)->hi;
_5 = b_12(D)->hi;
_6 = _4 + _5;
_7 = _16 != 0;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
# DEBUG s$hi => _9
MEM[(struct pair *)&D.2410] = _3;
MEM[(struct pair *)&D.2410 + 8B] = _9;
Имеем
_7 = _1 > _3;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
vs.
_7 = _16 != 0;
_8 = (long unsigned int) _7;
_9 = _6 + _8;
не тут ли потерялся перенос?
Также если открыть 311t.statistics, то можно заметить что
266 combine «three-insn combine» «pair add(pair&, pair&)» 1
не применяется. Возможно это следствие.
Возможно. Но я просто о том, что странные эффекты могут возникать в любом компиляторе. В случае с этой конкретной проблемой мы просто «забили», так как наш проект (по многим причинам, но в основном чтобы использовать libc++, а не libstdc++) переехал на clang. Соответственно «ездить по мозгам» разработчикам GCC оказалось некому…
Занимательная статья — вступление и выводы вроде более или менее просты в понимании, но вот целый параграф с момента "немного забегая вперед" выхреначил из контекста вон. Все равно после прочтения возникло большое желание еще почитать про выравнивание инструкций.
было бы все так очевидно,
в свое время пытался максимально оптимизировать, выработались некоторые практики, типо: такие структуры лучше обходить через for, другие — foreach, для больших структур в некоторых местах применить указатели и т.д. и т.п… потом столкнулся с проблемой: желание выработать «культуру письма» кончилось сломаной головой =)…
в результате: усложнение алгоритмов желанием минимум использования хеш-таблиц, пытаться обходить структуру не так как задумали разработчики языка и.т.п, куча головной боли себе придумал, и все ради нескольких мс к скорости выполнения программы… и в некоторых случаях применяемые методы давали сбой, и кроме сложности кода прога замедлялась в несколько раз)
больше такой херней не занимаюсь, и как в первом комменте написали: оптимизацию оставить разработчикам компиляторов, программисту нужно логику программы описывать, а не средствами ЯП пытаться залезть под капот компилятору =)
PS: занимательная статья
в свое время пытался максимально оптимизировать, выработались некоторые практики, типо: такие структуры лучше обходить через for, другие — foreach, для больших структур в некоторых местах применить указатели и т.д. и т.п… потом столкнулся с проблемой: желание выработать «культуру письма» кончилось сломаной головой =)…
в результате: усложнение алгоритмов желанием минимум использования хеш-таблиц, пытаться обходить структуру не так как задумали разработчики языка и.т.п, куча головной боли себе придумал, и все ради нескольких мс к скорости выполнения программы… и в некоторых случаях применяемые методы давали сбой, и кроме сложности кода прога замедлялась в несколько раз)
больше такой херней не занимаюсь, и как в первом комменте написали: оптимизацию оставить разработчикам компиляторов, программисту нужно логику программы описывать, а не средствами ЯП пытаться залезть под капот компилятору =)
PS: занимательная статья
Всё так, я своего нерадивого коллегу пытаюсь убедить, что использование ассемблерных вставок для матрасчётов в современном C++ — это моветон. Успехи так себе, конечно, с восклицаниями «Видишь? ВИДИШЬ!?» меня парировали тем, что при -O1 и -O3 перемешивание операндов в выражении позволяло уменьшить выражение на две инструкции, а на асме — ещё на одну. «С ПЛАВАЮЩЕЙ ЗАПЯТОЙ!!!»
Но мы ведь не про это, а про использование компилятора, который имеет просто неприличное множество настроек оптимизации (даже если это MSVC). Раз они есть и даются нам, будет глупо их игнорировать. Да, поиск оптимальных настроек полным перебором — это через чур, но потеребонькать основные оптимизирующие опции всё же стоит. Задачи мы решаем разные, платформы у нас разные и цели у нас разные, а компилятор один на всех — и не очень смышлёный. Нужно давать ему подсказки.
Но мы ведь не про это, а про использование компилятора, который имеет просто неприличное множество настроек оптимизации (даже если это MSVC). Раз они есть и даются нам, будет глупо их игнорировать. Да, поиск оптимальных настроек полным перебором — это через чур, но потеребонькать основные оптимизирующие опции всё же стоит. Задачи мы решаем разные, платформы у нас разные и цели у нас разные, а компилятор один на всех — и не очень смышлёный. Нужно давать ему подсказки.
>> Всё так, я своего нерадивого коллегу пытаюсь убедить, что использование ассемблерных вставок для матрасчётов в современном C++ — это моветон.
Инлайн-асмом что-ли? Зачем, когда есть инстринсики?
Так что либо интринсики, либо нормальный асм (если заставить компилятор генерить внятный код не получается).
Инлайн-асмом что-ли? Зачем, когда есть инстринсики?
Так что либо интринсики, либо нормальный асм (если заставить компилятор генерить внятный код не получается).
> Успехи так себе
За это надо сразу бить Кнутом
«premature optimization is the root of all evil»
За это надо сразу бить Кнутом
«premature optimization is the root of all evil»
А можно ссылку на оригинал? Хочется поделиться с иностранными коллегами
https://dendibakh.github.io/blog/2018/01/18/Code_alignment_issues
P.S.: ссылка есть в самом начале статьи.
Я удивлен что clang не применяет выравнивание по умолчанию. Тем более что его реккомендуют делать сами производители процессоров (например читал про такое в cortex-a57 optimization guide).
В gcc такие выравнивания делаются автоматически на -О2 и выше.
gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
Флаги -falign-…
Скорее всего это баг в clang. Возможно его стоит зарепортить разработчикам.
В gcc такие выравнивания делаются автоматически на -О2 и выше.
gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
Флаги -falign-…
Скорее всего это баг в clang. Возможно его стоит зарепортить разработчикам.
Тогда уж это в LLVM баг, clang же не занимается генерацией машинного кода самостоятельно.
По умолчанию выравнивает на 16 байт — размер instruction fetch (у intel).
Это видно в первом же листинге
4046c0: entry
4046d0: loop start
Если каждый цикл на 32 байта выравнивать, вырастет размер кода + что-то может наоборот не влезть в DSB или сместиться и вызвать замедление. В презентации Zia Ansari про это говорилось.
Это видно в первом же листинге
4046c0: entry
4046d0: loop start
Если каждый цикл на 32 байта выравнивать, вырастет размер кода + что-то может наоборот не влезть в DSB или сместиться и вызвать замедление. В презентации Zia Ansari про это говорилось.
Sign up to leave a comment.
Выравнивание инструкций кода