Оптимизация GCC: как уменьшить количество операций меньше?

При попытке сравнить некоторые параметры моего кода (используя 128-битные целые числа или нет) я наблюдал поведение, которое я просто не могу понять. Может ли кто-нибудь пролить свет на это?

#include <stdio.h>
#include <stdint.h>
#include <time.h>

int main(int a, char** b)
{
    printf("Running tests\n");

    clock_t start = clock();
    unsigned __int128 t = 13;
    for(unsigned long i = 0; i < (1UL<<30); i++)
        t += 23442*t + 25;
    if(t == 0) printf("0\n");
    printf("u128, +25, took %fs\n", double(clock() - start)/CLOCKS_PER_SEC);

    start = clock();
    t = 13;
    for(unsigned long i = 0; i < (1UL<<30); i++)
        t += 23442*t;
    if(t == 0) printf("0\n");
    printf("u128, no+, took %fs\n", double(clock() - start)/CLOCKS_PER_SEC);

    start = clock();
    unsigned long u = 13;
    for(unsigned long i = 0; i < (1UL<<30); i++)
        u += 23442*u + 25;
    if(u == 0) printf("0\n");
    printf("u64 , +25, took %fs\n", double(clock() - start)/CLOCKS_PER_SEC);

    start = clock();
    u = 13;
    for(unsigned long i = 0; i < (1UL<<30); i++)
        u += 23442*u;
    if(u == 0) printf("0\n");
    printf("u64 , no+, took %fs\n", double(clock() - start)/CLOCKS_PER_SEC);

    return 0;
}

(Обратите внимание, что printf здесь, так что gcc не оптимизирует цикл for) В моей системе это надежно производит следующий вывод:

u128, +25, took 2.411922s
u128, no+, took 1.799805s
u64 , +25, took 1.797960s
u64 , no+, took 2.454104s

В то время как 128-битное целочисленное поведение имеет смысл, я не вижу, как бит 64-разрядного цикла с меньшим количеством операций выполняет значительно (30%) медленнее.

Это известное поведение? Каково было бы общее правило, пытаясь извлечь выгоду из этой оптимизации при написании таких циклов?

Изменить: поведение наблюдается только при компиляции с опцией -O3.

gcc -lstdc++ -O3 -o a main.cpp

u128, +25, took 2.413949s
u128, no+, took 1.799469s
u64 , +25, took 1.798278s
u64 , no+, took 2.453414s

gcc -lstdc++ -O2 -o a main.cpp

u128, +25, took 2.415244s
u128, no+, took 1.800499s
u64 , +25, took 1.798699s
u64 , no+, took 1.348133s

Ответ 1

Цикл настолько плотный, что задержка зависимостей, занятость и ожидание ALU приходят в игру и доминируют над временем. Таким образом, результат не является надежным и более чувствительным к другим факторам, чем фактическое выполнение инструкции.

Обратите внимание, что +25 можно вычислить параллельно вместе с умножением.


PS. Мой результат на 4970K:

gcc version 5.2.1 20151010
gcc -lstdc++ -O2 -o a a.cpp

u128, +25, took 1.346360s
u128, no+, took 1.022965s
u64 , +25, took 1.020189s
u64 , no+, took 0.765725s

РЕДАКТИРОВАТЬ: после просмотра в дизассемблировании на -O2 и -O3 основное различие заключается в генерации кода. (Выше рассудка все еще держится -O2 по сравнению с другими тестовыми машинами/средой, дающими несколько разные результаты)

-O2:

400618:       48 69 d2 93 5b 00 00    imul   $0x5b93,%rdx,%rdx
40061f:       48 83 e8 01             sub    $0x1,%rax
400623:       75 f3                   jne    400618 <_Z4testv+0x18>

-O3:

400628:       66 0f 6f d9             movdqa %xmm1,%xmm3
40062c:       83 c0 01                add    $0x1,%eax
40062f:       66 0f 6f c1             movdqa %xmm1,%xmm0
400633:       66 0f f4 cc             pmuludq %xmm4,%xmm1
400637:       3d 00 00 00 20          cmp    $0x20000000,%eax
40063c:       66 0f f4 da             pmuludq %xmm2,%xmm3
400640:       66 0f 73 d0 20          psrlq  $0x20,%xmm0
....

O3 генерирует векторизованный код, в то время как цикл имеет большую зависимость, которая не может получить значение из векторизации. На самом деле он создавал гораздо более сложный код и, следовательно, имеет гораздо более длительный период времени.

Ответ 2

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

Вы можете использовать PAPI или Intel блестящие инструменты для выполнения измерений. Инструменты Intel стоят дорого, но вы можете попробовать их в течение 30 дней бесплатно. Они намного проще в использовании, чем PAPI.