Thursday, February 3, 2022

[SOLVED] Why -O1 is faster than -O2

Issue

I wrote a C code like this:

#include <stdio.h>
#define N 19

int main(void){
    int a[N];
    int ans = 0;

    for(int i = 0; i < N; ++i){
        a[i] = 0;
    }
    for(;;){
        int i;
        ++ans;
        for(i = N - 1; a[i] == 2; --i){
            if(i == 0){
                printf("%d\n", ans);
                return 0;
            }else{
                a[i] = 0;
            }
        }
        ++a[i];
    }
}

This counts the ways to choose N (= 19 ) numbers from 0 to 2, and prints the number of ways (= 3^19 = 1,162,261,467).

I compiled this code with gcc. -O1 was faster than -O2. Why -O2 optimization was worse than -O1?

  • CPU: Intel(R) Core(TM) i7-8565U, x86_64
  • OS: Arch Linux (5.9.1-arch1-1)
  • compiler: gcc (GCC) 10.2.0

Edit:

Running gcc with -S option produced the following assembly codes: -O1

    .file   "a.c"
    .text
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    subq    $104, %rsp
    .cfi_def_cfa_offset 112
    movq    %fs:40, %rax
    movq    %rax, 88(%rsp)
    xorl    %eax, %eax
    movq    %rsp, %rax
    leaq    76(%rsp), %rdx
.L2:
    movl    $0, (%rax)
    addq    $4, %rax
    cmpq    %rdx, %rax
    jne .L2
    movl    $0, %esi
    jmp .L7
.L4:
    movslq  %edx, %rdx
    addl    $1, %ecx
    movl    %ecx, (%rsp,%rdx,4)
.L7:
    addl    $1, %esi
    movl    72(%rsp), %ecx
    leaq    68(%rsp), %rax
    movl    $18, %edx
    cmpl    $2, %ecx
    jne .L4
.L5:
    movl    $0, 4(%rax)
    subl    $1, %edx
    movl    (%rax), %ecx
    cmpl    $2, %ecx
    jne .L4
    subq    $4, %rax
    testl   %edx, %edx
    jne .L5
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movq    88(%rsp), %rax
    subq    %fs:40, %rax
    jne .L14
    movl    $0, %eax
    addq    $104, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
.L14:
    .cfi_restore_state
    call    __stack_chk_fail@PLT
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (GNU) 10.2.0"
    .section    .note.GNU-stack,"",@progbits

-O2

    .file   "a.c"
    .text
    .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
    .string "%d\n"
    .section    .text.startup,"ax",@progbits
    .p2align 4
    .globl  main
    .type   main, @function
main:
.LFB11:
    .cfi_startproc
    subq    $104, %rsp
    .cfi_def_cfa_offset 112
    movl    $9, %ecx
    xorl    %esi, %esi
    movq    %fs:40, %rax
    movq    %rax, 88(%rsp)
    xorl    %eax, %eax
    movq    %rsp, %rdx
    movq    %rdx, %rdi
    rep stosq
    movl    $0, (%rdi)
    leaq    68(%rsp), %rdi
.L6:
    movl    72(%rsp), %ecx
    addl    $1, %esi
    movq    %rdi, %rax
    movl    $18, %edx
    cmpl    $2, %ecx
    je  .L4
    jmp .L3
    .p2align 4,,10
    .p2align 3
.L5:
    subq    $4, %rax
    testl   %edx, %edx
    je  .L14
.L4:
    movl    (%rax), %ecx
    movl    $0, 4(%rax)
    subl    $1, %edx
    cmpl    $2, %ecx
    je  .L5
.L3:
    movslq  %edx, %rdx
    addl    $1, %ecx
    movl    %ecx, (%rsp,%rdx,4)
    jmp .L6
    .p2align 4,,10
    .p2align 3
.L14:
    xorl    %eax, %eax
    leaq    .LC0(%rip), %rdi
    call    printf@PLT
    movq    88(%rsp), %rax
    subq    %fs:40, %rax
    jne .L15
    xorl    %eax, %eax
    addq    $104, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 8
    ret
.L15:
    .cfi_restore_state
    call    __stack_chk_fail@PLT
    .cfi_endproc
.LFE11:
    .size   main, .-main
    .ident  "GCC: (GNU) 10.2.0"
    .section    .note.GNU-stack,"",@progbits

And the benchmark is:

$ gcc a.c -O1
$ time ./a.out
1162261467

real    0m0.895s
user    0m0.894s
sys 0m0.000s
$ time ./a.out
1162261467

real    0m0.912s
user    0m0.911s
sys 0m0.000s
$ time ./a.out
1162261467

real    0m0.925s
user    0m0.924s
sys 0m0.001s
$ gcc a.c -O2
$ time ./a.out
1162261467

real    0m1.570s
user    0m1.568s
sys 0m0.000s
$ time ./a.out
1162261467

real    0m1.567s
user    0m1.562s
sys 0m0.004s
$ time ./a.out
1162261467

real    0m1.576s
user    0m1.568s
sys 0m0.001s
$ gcc a.c -O3
$ time ./a.out
1162261467

real    0m1.613s
user    0m1.612s
sys 0m0.000s
$ time ./a.out
1162261467

real    0m1.608s
user    0m1.599s
sys 0m0.003s
$ time ./a.out
1162261467

real    0m1.628s
user    0m1.628s
sys 0m0.000s
$ gcc a.c -Ofast
$ time ./a.out
1162261467

real    0m1.571s
user    0m1.570s
sys 0m0.001s
$ time ./a.out
1162261467

real    0m1.604s
user    0m1.595s
sys 0m0.004s
$ time ./a.out
1162261467

real    0m1.616s
user    0m1.613s
sys 0m0.000s
$ gcc a.c -O0
$ time ./a.out
1162261467

real    0m2.457s
user    0m2.456s
sys 0m0.001s
$ time ./a.out
1162261467

real    0m2.526s
user    0m2.525s
sys 0m0.000s
$ time ./a.out
1162261467

real    0m2.565s
user    0m2.565s
sys 0m0.000s

Edit:

I edited the code like this:

#include <stdio.h>
#define N 19

volatile int answer;

int main(void){
    int a[N];
    int ans = 0;

    for(int i = 0; i < N; ++i){
        a[i] = 0;
    }
    for(;;){
        int i;
        ++ans;
        for(i = N - 1; a[i] == 2; --i){
            if(i == 0){
                answer = ans;
                return 0;
            }else{
                a[i] = 0;
            }
        }
        ++a[i];
    }
}

And measured again:

$ gcc a.c -O1
$ time ./a.out

real    0m0.924s
user    0m0.924s
sys 0m0.000s
$ time ./a.out

real    0m0.950s
user    0m0.949s
sys 0m0.000s
$ time ./a.out

real    0m0.993s
user    0m0.989s
sys 0m0.004s
$ gcc a.c -O2
$ time ./a.out

real    0m1.637s
user    0m1.636s
sys 0m0.000s
$ time ./a.out

real    0m1.661s
user    0m1.656s
sys 0m0.004s
$ time ./a.out

real    0m1.656s
user    0m1.654s
sys 0m0.001s

Edit:

I added [[likely]] attribute after for(;;):

#include <stdio.h>
#define N 19

int main(void){
    int a[N];
    int ans = 0;

    for(int i = 0; i < N; ++i){
        a[i] = 0;
    }
    for(;;) [[likely]] {
        int i;
        ++ans;
        for(i = N - 1; a[i] == 2; --i){
            if(i == 0){
                printf("%d\n", ans);
                return 0;
            }else{
                a[i] = 0;
            }
        }
        ++a[i];
    }
}

Then, the result of benchmark changed:

$ g++ a.cpp -O1
$ for i in {1..5}; do time ./a.out; done
1162261467
./a.out  0.65s user 0.00s system 99% cpu 0.653 total
1162261467
./a.out  0.65s user 0.00s system 99% cpu 0.657 total
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.656 total
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.665 total
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.660 total
$ g++ a.cpp -O2
$ for i in {1..5}; do time ./a.out; done
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.661 total
1162261467
./a.out  0.65s user 0.00s system 99% cpu 0.648 total
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.659 total
1162261467
./a.out  0.65s user 0.00s system 99% cpu 0.654 total
1162261467
./a.out  0.66s user 0.00s system 99% cpu 0.657 total

-O2 is as fast as -O1! Thank you @Acorn .


Solution

Why -O2 optimization was worse than -O1?

Higher optimization levels should give you better performance in most cases. Nevertheless, you may find exceptions like this one. Particularly in micro-benchmarks like this one.

The code and data memory that your program uses is so small that caches and memory accesses are unlikely to be an issue. However, it is branch-heavy, which means it will come down to static and dynamic branching prediction.

If your compiler has got it wrong, like in this case, you can try to give it more information with likely/unlikely hints or profiling the program.



Answered By - Acorn
Answer Checked By - Senaida (WPSolving Volunteer)