четверг, 22 ноября 2012 г.

О вреде методов объектов в C

В предыдущем посте я показал как в Си можно использовать вызов функций в стиле, похожем на C++. Теперь об основной причине, по которой так делать не стоит.
Возьмём предыдущий пример с точкой и замерим его скорость в обоих вариантах. Рабочий код будет таким:

#include <stdlib.h>

typedef struct _Point {
    void (*moveBy) (struct _Point * self, int x, int y);

    float x;
    float y;
} Point;

void movePointBy(Point * p, int x, int y) {
    p->x += x;
    p->y += y;
}

int main() {
    Point p;
    long int i;

    p.moveBy = movePointBy;

    srand(100);
    for(i = 0; i < 1000000000; i++)
    {
        movePointBy(&p, rand(), rand());  // для файла main.c
// p.moveBy(&p, rand(), rand());             // для файла main2.c
    }

    return 0;
}


Количество итераций такое чтобы можно было заметить разницу, функция rand чтобы процессору было сложнее предсказывать.

Замерим  скорость:

alex@comp ~/test/test4 $ gcc -std=c89 -pedantic -Wall main.c -O0 && time ./a.out

real    0m20.346s
user    0m20.329s
sys     0m0.000s

alex@comp ~/test/test4 $ gcc -std=c89 -pedantic -Wall main.c -O3 && time ./a.out

real    0m16.256s
user    0m16.239s
sys     0m0.001s

alex@comp ~/test/test4 $ gcc -std=c89 -pedantic -Wall main2.c -O0 && time ./a.out

real    0m20.352s
user    0m20.334s
sys     0m0.000s

alex@comp ~/test/test4 $ gcc -std=c89 -pedantic -Wall main2.c -O3 && time ./a.out

real    0m18.615s
user    0m18.420s
sys     0m0.169s


Как видно, на уровне оптимизации -O3 начинается отставание по скорости. Собственно, это и есть причина почему так делать не стоит. По рассказам, из-за подобной ошибки в программе на плюсах, когда вместо автоматического объекта в качестве счётчика использовался член класса, производительность падала на порядок!

Теперь посмотрим, почему так произошло:

В неоптимизированных версиях основное различие в способе вызова функции:

.L4:
    call    rand
    movl    %eax, %ebx
    call    rand
    movl    %eax, %ecx
    leaq    -48(%rbp), %rax
    movl    %ebx, %edx
    movl    %ecx, %esi
    movq    %rax, %rdi
    call    movePointBy
    addq    $1, -24(%rbp)
.L3:
    cmpq    $999999999, -24(%rbp)
    jle    .L4
    movl    $0, %eax
    addq    $40, %rsp
    popq    %rbx
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc


против

.L4:
    movq    -48(%rbp), %r12
    call    rand
    movl    %eax, %ebx
    call    rand
    movl    %eax, %ecx
    leaq    -48(%rbp), %rax
    movl    %ebx, %edx
    movl    %ecx, %esi
    movq    %rax, %rdi
    call    *%r12
    addq    $1, -24(%rbp)
.L3:
    cmpq    $999999999, -24(%rbp)
    jle    .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbx
    popq    %r12
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc


Как видим, оно не особо сказалось на скорости выполнения. Но оно сказалось на качестве оптимизаций. Во второй версии приходится руками заботиться о сохранности стековых и базовых регистров. Поэтому, если для версии с просто вызовом мы получили такой код после оптимизации:

.L3:
    call    rand
    call    rand
    subq    $1, %rbx
    jne    .L3
    xorl    %eax, %eax
    popq    %rbx
    .cfi_def_cfa_offset 8
    .p2align 4,,1
    ret
    .cfi_endproc


то для вызова по указателю есть куча всякой лабуды, которую нельзя удалить:

.L3:
    movq    (%rsp), %rbp
    call    rand
    movl    %eax, %r13d
    call    rand
    movl    %r13d, %edx
    movl    %eax, %esi
    movq    %rsp, %rdi
    call    *%rbp
    subq    $1, %rbx
    jne    .L3
    addq    $24, %rsp
    .cfi_def_cfa_offset 40
    xorl    %eax, %eax
    popq    %rbx
    .cfi_def_cfa_offset 32
    popq    %rbp
    .cfi_def_cfa_offset 24
    popq    %r12
    .cfi_def_cfa_offset 16
    popq    %r13
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc


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

1 комментарий:

  1. Единственное адекватное применение этого --- виртуальные методы.
    Разница аналогична нормальному vs девиртуализованному вызову в плюсах. И возможности девиртуализации примерно аналогичны.

    ОтветитьУдалить