Подтвердить что ты не робот

Почему clang производит гораздо более быстрый код, чем gcc для этой простой функции, связанной с возведением в степень?

Следующий код, скомпилированный с clang, работает почти в 60 раз быстрее, чем тот, скомпилированный с помощью gcc с одинаковыми флагами компилятора (либо -O2, либо -O3):

#include <iostream>
#include <math.h> 
#include <chrono>
#include <limits>

long double func(int num)
{
    long double i=0;
    long double k=0.7;

    for(int t=1; t<num; t++){
      for(int n=1; n<16; n++){
        i += pow(k,n);
      }
    }
    return i;
}


int main()
{
   volatile auto num = 3000000; // avoid constant folding

   std::chrono::time_point<std::chrono::system_clock> start, end;
   start = std::chrono::system_clock::now();

   auto i = func(num);

   end = std::chrono::system_clock::now();
   std::chrono::duration<double> elapsed = end-start;
   std::cout.precision(std::numeric_limits<long double>::max_digits10);
   std::cout << "Result " << i << std::endl;
   std::cout << "Elapsed time is " << elapsed.count() << std::endl;

   return 0;
}

Я тестировал это с тремя версиями gcc 4.8.4/4.9.2/5.2.1 и двумя версиями clang 3.5.1/3.6.1, и вот тайминг на моей машине (для gcc 5.2.1 и clang 3.6.1):

Сроки -O3:

gcc:    2.41888s
clang:  0.0396217s 

Сроки -O2:

gcc:    2.41024s
clang:  0.0395114s 

Сроки -O1:

gcc:    2.41766s
clang:  2.43113s

Итак, кажется, что gcc вообще не оптимизирует эту функцию даже на более высоких уровнях оптимизации. Вывод сборки clang составляет почти около 100 строк дольше, чем gcc, и я не думаю, что это необходимо, чтобы опубликовать его здесь, все, что я могу сказать, заключается в том, что на выходе сборки gcc есть вызов pow который не появляется в сборке clang, предположительно потому, что clang оптимизирует его до кучи внутренних вызовов.

Поскольку результаты идентичны (т.е. i = 6966764.74717416727754), возникает вопрос:

  • Почему gcc не может оптимизировать эту функцию, если clang может?
  • Измените значение k на 1.0 и gcc станет таким же быстрым, есть ли арифметическая проблема с плавающей запятой, которую gcc не может пропустить?

Я попробовал static_cast ing и включил предупреждения, чтобы узнать, есть ли какая-либо проблема с неявными преобразованиями, но на самом деле.

Обновление: Для полноты здесь приведены результаты для -Ofast

gcc:    0.00262204s
clang:  0.0013267s

Дело в том, что gcc не оптимизирует код при O2/O3.

4b9b3361

Ответ 1

Из этого godbolt session clang может выполнять все вычисления pow во время компиляции. Во время компиляции известно, что значения k и n есть, и он просто сгибает расчет:

.LCPI0_0:
    .quad   4604480259023595110     # double 0.69999999999999996
.LCPI0_1:
    .quad   4602498675187552091     # double 0.48999999999999994
.LCPI0_2:
    .quad   4599850558606658239     # double 0.34299999999999992
.LCPI0_3:
    .quad   4597818534454788671     # double 0.24009999999999995
.LCPI0_4:
    .quad   4595223380205512696     # double 0.16806999999999994
.LCPI0_5:
    .quad   4593141924544133109     # double 0.11764899999999996
.LCPI0_6:
    .quad   4590598673379842654     # double 0.082354299999999963
.LCPI0_7:
    .quad   4588468774839143248     # double 0.057648009999999972
.LCPI0_8:
    .quad   4585976388698138603     # double 0.040353606999999979
.LCPI0_9:
    .quad   4583799016135705775     # double 0.028247524899999984
.LCPI0_10:
    .quad   4581356477717521223     # double 0.019773267429999988
.LCPI0_11:
    .quad   4579132580613789641     # double 0.01384128720099999
.LCPI0_12:
    .quad   4576738892963968780     # double 0.0096889010406999918
.LCPI0_13:
    .quad   4574469401809764420     # double 0.0067822307284899942
.LCPI0_14:
    .quad   4572123587912939977     # double 0.0047475615099429958

и он разворачивает внутренний цикл:

.LBB0_2:                                # %.preheader
    faddl   .LCPI0_0(%rip)
    faddl   .LCPI0_1(%rip)
    faddl   .LCPI0_2(%rip)
    faddl   .LCPI0_3(%rip)
    faddl   .LCPI0_4(%rip)
    faddl   .LCPI0_5(%rip)
    faddl   .LCPI0_6(%rip)
    faddl   .LCPI0_7(%rip)
    faddl   .LCPI0_8(%rip)
    faddl   .LCPI0_9(%rip)
    faddl   .LCPI0_10(%rip)
    faddl   .LCPI0_11(%rip)
    faddl   .LCPI0_12(%rip)
    faddl   .LCPI0_13(%rip)
    faddl   .LCPI0_14(%rip)

Обратите внимание, что он использует встроенную функцию (gcc документирует их здесь), чтобы вычислить pow во время компиляции, и если мы используем - fno-builtin больше не выполняет эту оптимизацию.

Если вы измените k на 1.0, то gcc может выполнить ту же оптимизацию:

.L3:
    fadd    %st, %st(1) #,
    addl    $1, %eax    #, t
    cmpl    %eax, %edi  # t, num
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    jne .L3 #,

Хотя это более простой случай.

Если вы измените условие для внутреннего цикла на n < 4, то gcc кажется желающим оптимизировать, когда k = 0.7. Как указано в комментариях к вопросу, если компилятор не считает, что разворачивание поможет, это, скорее всего, будет консервативным в том, насколько он будет развернут, потому что есть компромисс.

Как указано в комментариях, я использую модифицированную версию кода OP в примерах godbolt, но это не меняет основной вывод.

Обратите внимание, как указано в комментарии выше, если мы используем - fno-math-errno, который останавливает установку errno, gcc применяет аналогичную оптимизацию.

Ответ 2

В дополнение к отвечу Шафика Ягмура, я хотел бы указать, что причина, по которой ваше использование volatile для переменной num, похоже, не влияет на то, что num читается до func равно называется. Чтение не может быть оптимизировано, но вызов функции может быть оптимизирован. Если вы указали параметр func ссылкой на volatile, т.е. long double func(volatile int& num), это предотвратит компилятор от оптимизации всего вызова до func.