В предыдущем посте я показал как в Си можно использовать вызов функций в стиле, похожем на C++. Теперь об основной причине, по которой так делать не стоит.
Возьмём предыдущий пример с точкой и замерим его скорость в обоих вариантах. Рабочий код будет таким:
Количество итераций такое чтобы можно было заметить разницу, функция rand чтобы процессору было сложнее предсказывать.
Замерим скорость:
Как видно, на уровне оптимизации -O3 начинается отставание по скорости. Собственно, это и есть причина почему так делать не стоит. По рассказам, из-за подобной ошибки в программе на плюсах, когда вместо автоматического объекта в качестве счётчика использовался член класса, производительность падала на порядок!
Теперь посмотрим, почему так произошло:
В неоптимизированных версиях основное различие в способе вызова функции:
против
Как видим, оно не особо сказалось на скорости выполнения. Но оно сказалось на качестве оптимизаций. Во второй версии приходится руками заботиться о сохранности стековых и базовых регистров. Поэтому, если для версии с просто вызовом мы получили такой код после оптимизации:
то для вызова по указателю есть куча всякой лабуды, которую нельзя удалить:
Можно, конечно, сказать, что программы редко собираются на -O3 и даже на -O2, но возможность повышения скорости, в том числе при помощи оптимизаций - одно из основных преимуществ компилируемых языков и часто отказываться от него не имеет смысла.
Возьмём предыдущий пример с точкой и замерим его скорость в обоих вариантах. Рабочий код будет таким:
#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, но возможность повышения скорости, в том числе при помощи оптимизаций - одно из основных преимуществ компилируемых языков и часто отказываться от него не имеет смысла.
Единственное адекватное применение этого --- виртуальные методы.
ОтветитьУдалитьРазница аналогична нормальному vs девиртуализованному вызову в плюсах. И возможности девиртуализации примерно аналогичны.