1

Тема: Постінкремент швидший за преінкремент?

Цікаво було перевірити що ж швидше.
В результаті замірів швидкості компайлер Clang видав цікаві графіки.
Де постінкрмент був в біль ніж 5 разів швидшим за преінкремент.
Як думаєте, це бага компайлера чи GoogleBench'а?

2

Re: Постінкремент швидший за преінкремент?

Якщо натиснути "Open in Compiler Explorer" (перехід на Godbolt), то вилазить такий код для тестових циклів цих двох варіантів:

.LBB0_3:                                # =>This Inner Loop Header: Depth=1
        add     qword ptr [rsp + 8], 1
        add     rbx, -1
        jne     .LBB0_3

i

.LBB1_3:                                # =>This Inner Loop Header: Depth=1
        lea     rcx, [rax + 1]
        mov     rax, rcx
        cmp     rbx, rcx
        jne     .LBB1_3

Як бачите, в першому випадку компілятор наполіг на тому, щоб зберегти x як фізично існуючу змінну в пам'яті. А в другому випадку представив її регістром. Звідси й різниця в продуктивності. Я б припустив, що це наслідок того, що результат преінкремента в С++ є lvalue. І цей тестбенч з якоїсь причини наполягає на тому, щоб зберегти lvalue-ність результату. Швидше за все це навіть фіча, тобто документована властивість benchmark::DoNotOptimize, про яку потрібно пам'ятати.

Якщо в першому випадку "допомогти" компілятору зрозуміти, що нам нафіг не потрібна lvalue-ність результату ++x, наприклад за допомогою явного приведення типу

benchmark::DoNotOptimize(std::size_t{++x});

, то результати виходять набагато рівніші за продуктивністю.

Подякували: Arete, koala, Naomi3

3

Re: Постінкремент швидший за преінкремент?

Так перевіряти ж треба розумно.
benchmark::DoNotOptimize, як я розумію, просто фіксує змінну, щоб оптимізатор її не викинув. А оскільки там параметром іде не змінна, а результат оператора, наслідки дещо неочікувані.
Ось тест, що показує, що вони однакові.
Ну і найголовніше: безглуздо казати про "швидше взагалі", треба дивитися конкретні ситуації. Конкретно в першій ситуації швидший постінкремент, так склалося. Без оптимізації (-O0), гадаю, швидшим буде преінкремент.

Глянув асемблер - щось оптимізатор LLVM себе переграв. Постінкремент виглядає так:

       %lea    0x1(%rax),%rcx
       mov    %rcx,%rax
       cmp    %rcx,%rbx
       jne    211740 <PostInc(benchmark::State&)+0x20>

а преінкремент

       addq   $0x1,0x8(%rsp)
       add    $0xffffffffffffffff,%rbx
       jne    211700 <PreInc(benchmark::State&)+0x40>

Схоже, що тут в преінкременті LLVM намагається записувати стан змінної у пам'ять, чому й гальмує.