Я вперше помітив у 2009 році, що GCC (принаймні, на моїх проектах і на моїх машинах) має тенденцію генерувати помітно швидший код, якщо я оптимізую розмір ( -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 с, а якщо вона компілюється з -O2
або 0,44 с -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 прокладки функціонує з 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,37с-O2 -fno-align-functions -fno-align-loops
0,37с-S -O2
потім ручне переміщення збіркиadd()
післяwork()
0,37 с-O2
0,44с
Мені здається, що відстань 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
, а не лише ті, що наведені вище. Пропуски кеш-пам’яті дуже шумні і майже не виявляють кореляції з часом виконання.