Ненужные всплывающие инструкции в функциях с ранним оператором if

во время игры с godbolt.org я заметил, что gcc (6.2, снапшот 7.0), clang (3.9) и icc (17) при компиляции чего-то близкого к

int a(int* a, int* b) {
  if (b - a < 2) return *a = ~*a;

  // register intensive code here e.g. sorting network
}

компилирует (-O2/-O3) это примерно так:

    push    r15
    mov     rax, rcx
    push    r14
    sub     rax, rdx
    push    r13
    push    r12
    push    rbp
    push    rbx
    sub     rsp, 184
    mov     QWORD PTR [rsp], rdx
    cmp     rax, 7
    jg      .L95
    not     DWORD PTR [rdx]
 .L162:
    add     rsp, 184
    pop     rbx
    pop     rbp
    pop     r12
    pop     r13
    pop     r14
    pop     r15
    ret

что, очевидно, имеет огромные накладные расходы в случае b - a ‹ 2. В случае -Os gcc компилируется в:

    mov     rax, rcx
    sub     rax, rdx
    cmp     rax, 7
    jg      .L74
    not     DWORD PTR [rdx]
    ret
.L74:

Это наводит меня на мысль, что нет кода, который мешает компилятору выдать этот более короткий код.

Есть ли причина, почему компиляторы делают это? Есть ли способ компилировать их в более короткую версию без компиляции по размеру?


Here's an example on Godbolt that reproduces this. Кажется, это как-то связано с рекурсивностью сложной части.


person Christoph Diegelmann    schedule 20.10.2016    source источник
comment
Кстати: x86-64 clang 3.9.0 компилируется в очень короткую версию с -O1 выделенным жирным шрифтом, что несколько противоречит тому, что вы пишете в своем вопросе. Вы не должны писать при компиляции чего-то близкого к, но при компиляции и отправлять фактический код или минимально воспроизводимый пример.   -  person Jabberwocky    schedule 20.10.2016
comment
Это более или менее известная слабость современных компиляторов. Было бы намного лучше выполнить предварительный тест перед сохранением всех регистров, которые потребуются всей функции, чтобы их не нужно было извлекать. Одним из обходных путей является перенос раннего теста в функцию-оболочку. Это особенно полезно, если оболочка может быть встроенной.   -  person Peter Cordes    schedule 20.10.2016
comment
Вы можете получить то, что хотите, с if (__bultin_expect(b - a < 2, 1)). Кроме того, доступность функции для встраивания (или использование оптимизации всей программы) может позволить GCC частично встроить оператор if.   -  person Ross Ridge    schedule 20.10.2016
comment
Было бы хорошо, если бы вы могли создать реальный пример, который компилируется, и связать его с Godbolt (с полной ссылкой, чтобы предотвратить гниение ссылок из-за сокращения URL-адреса). Однако то, что вы показали в вопросе, хорошо подходит в качестве примера. @MichaelWalz: Вы постоянно видите подобный код, если смотрите на вывод компилятора в реальном коде. Конечно, вы получите тривиальную функцию, если пропустите часть //... complicated-code-here.   -  person Peter Cordes    schedule 20.10.2016
comment
@MichaelWalz, фактический код составляет около 800 строк и в настоящее время недоступен для публики. Я пытаюсь сделать минимальный код, воспроизводящий проблему, но это довольно сложно.   -  person Christoph Diegelmann    schedule 20.10.2016
comment
@Christoph: просто найдите совершенно другую функцию в чем-то с открытым исходным кодом, у которого есть ранний выход наверху. Кстати, это раздувание быстрого пути является одним из вариантов использования __attribute__((noinline)) в ядре Linux: поместите общий регистр с интенсивным регистром с обработкой ошибок в другую функцию и предотвратите ее встраивание, чтобы не было push/pop на быстрый путь. (Где быстрый путь через функцию довольно короткий, как ваш ранний выход.)   -  person Peter Cordes    schedule 20.10.2016
comment
@Cordes Кажется, это связано с рекурсией. Я сделал доступным короткий фрагмент кода на godbolt.   -  person Christoph Diegelmann    schedule 20.10.2016
comment
Если это когда он рекурсирует, то наиболее вероятно, что оператор if будет истинным, тогда разделение функции, как предлагает Питер Кордес, должно работать хорошо, даже если оболочка встроена только в функцию с интенсивным регистром.   -  person Ross Ridge    schedule 20.10.2016


Ответы (1)


Это известное ограничение компилятора, см. мои комментарии к вопросу. ИДК, почему он существует; возможно, компиляторам трудно решить, что они могут сделать без сброса, когда они еще не закончили сохранение regs.

Вытягивание раннего чека в оболочку часто полезно, когда он достаточно мал, чтобы его можно было встроить.


Похоже, что современный gcc иногда может обойти это ограничение компилятора.

Используя ваш пример в обозревателе компилятора Godbolt, добавление второго вызывающего объекта достаточно, чтобы даже gcc6.1 -O2 разделил функцию для вас, чтобы он мог встроить ранний выход во второй вызывающий объект и во внешне видимый square() (который заканчивается на jmp square(int*, int*) [clone .part.3], если путь возврата досрочно не используется).

code on Godbolt, note I added -std=gnu++14, which is required for clang to compiler your code.

void square_inlinewrapper(int* a, int* b) {
  //if (b - a < 16) return;  // gcc inlines this part for us, and calls a private clone of the function!

  return square(a, b);
}

# gcc6.1 -O2  (default / generic -march= and -mtune=)
    mov     rax, rsi
    sub     rax, rdi
    cmp     rax, 63
    jg      .L9
    rep ret
.L9:
    jmp     square(int*, int*) [clone .part.3]

square() сам компилируется в то же самое, вызывая частный клон, который содержит большую часть кода. Рекурсивные вызовы внутри клона вызывают функцию-оболочку, поэтому они не выполняют дополнительную работу push/pop, когда она не нужна.


Даже gcc7 не делает этого, когда нет другого вызывающего объекта, даже в -O3. Он по-прежнему преобразует один из рекурсивных вызовов в цикл, а другой просто снова вызывает большую функцию.


Clang 3.9 и icc17 также не клонируют функцию, поэтому вам следует написать встроенную оболочку вручную (и изменить основное тело функции, чтобы использовать его для рекурсивных вызовов, если там нужна проверка).

Возможно, вы захотите назвать оболочку square и переименовать только основное тело в личное имя (например, static void square_impl).

person Peter Cordes    schedule 20.10.2016
comment
Знаете ли вы, собирал ли кто-то такие известные ограничения компилятора? - person Christoph Diegelmann; 20.10.2016
comment
@Christoph: я нигде не знаю списка. Было бы очень сложно поддерживать что-либо подобное в актуальном состоянии, поскольку каждая запись, по сути, была бы ошибкой пропущенной оптимизации, которую можно было бы исправить в любое время для одного компилятора, но не для других. Вы можете выполнить поиск gcc по запросу ошибки, связанные с пропущенной оптимизацией, и получите список, который достигает предела в 500 результатов поиска... - person Peter Cordes; 21.10.2016