Я впервые заметил в 2009 году, что GCC (по крайней мере, в моих проектах и на моих машинах) имеет тенденцию генерировать заметно более быстрый код, если я оптимизирую для size ( -Os
) вместо скорости ( -O2
или -O3
), и с тех пор я удивляюсь, почему.
Мне удалось создать (довольно глупый) код, который демонстрирует это удивительное поведение и достаточно мал, чтобы быть размещенным здесь.
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
Если я скомпилирую его -Os
, выполнение этой программы займет 0,38 с и 0,44 с, если она скомпилирована с помощью -O2
или -O3
. Эти времена получены последовательно и практически без помех (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Обновление: я переместил весь ассемблерный код на GitHub : они сделали публикацию раздутой и, по-видимому, добавили очень мало значения к вопросам, поскольку fno-align-*
флаги имеют тот же эффект.)
Вот сгенерированная сборка с -Os
и -O2
.
К сожалению, мое понимание сборки очень ограничено, так что я понятия не имею ли то , что я делал дальше , было правильно: я схватил сборку для -O2
и объединить все свои различия в сборку за -Os
исключением тех .p2align
линий, результат здесь . Этот код по-прежнему работает в 0.38 с, и единственное отличие состоит в .p2align
материале.
Если я правильно угадал, это отступы для выравнивания стека. Согласно Почему GCC pad работает с NOP? это сделано в надежде, что код будет работать быстрее, но, очевидно, эта оптимизация не принесла результатов в моем случае.
В этом случае виновником является прокладка? Почему и как?
Шум, который он издает, делает невозможным микро-оптимизацию синхронизации.
Как я могу убедиться, что такие случайные удачные / неудачные выравнивания не мешают, когда я выполняю микрооптимизации (не связанные с выравниванием по стеку) в исходном коде C или C ++?
ОБНОВИТЬ:
После ответа Паскаля Куока я немного повозился с выравниванием. Переходя -O2 -fno-align-functions -fno-align-loops
к gcc, все .p2align
уходит из сборки, и сгенерированный исполняемый файл запускается за 0.38 с. Согласно документации gcc :
-Os включает все оптимизации -O2 [но] -Os отключает следующие флаги оптимизации:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Таким образом, это в значительной степени похоже на (неправильную) проблему выравнивания.
Я все еще скептически отношусь к тому, -march=native
что было предложено в ответе Марата Духана . Я не уверен, что это не только мешает этой (неправильной) проблеме выравнивания; это абсолютно не влияет на мою машину. (Тем не менее, я проголосовал за его ответ.)
ОБНОВЛЕНИЕ 2:
Мы можем взять -Os
из картины. Следующие времена получены путем компиляции с
-O2 -fno-omit-frame-pointer
0.37s-O2 -fno-align-functions -fno-align-loops
0.37s-S -O2
затем вручную перемещая сборкуadd()
черезwork()
0,37 с-O2
0.44s
Похоже, для меня add()
большое значение имеет расстояние от сайта вызова. Я пытался perf
, но вывод perf stat
и perf report
имеет очень мало смысла для меня. Тем не менее, я мог получить только один последовательный результат из этого:
-O2
:
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Для fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Для -fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Похоже, мы остановились на вызове add()
в медленном случае.
Я изучил все, что perf -e
может выплюнуть на моей машине; не только статистика, которая приведена выше.
Для того же исполняемого файла stalled-cycles-frontend
показана линейная корреляция со временем выполнения; Я не заметил ничего другого, что так четко соотносилось бы. (Сравнение stalled-cycles-frontend
для разных исполняемых файлов не имеет смысла для меня.)
Я включил пропуски кэша, так как он появился в качестве первого комментария. Я изучил все ошибки кэша, которые можно измерить на моей машине perf
, а не только те, которые приведены выше. Промахи в кеше очень шумные и практически не коррелируют со временем выполнения.