Ответ 1
Что заставляет вас думать, что накладные расходы на vtable составляют 20 циклов? Если это действительно так, вам нужен лучший компилятор С++.
Я пробовал это на ящике Intel, ничего не зная о процессоре, который вы используете, и, как и ожидалось, разница между кодом отправки и отправкой С++ vtable является одной инструкцией, связанной с тем, что vtable включает в себя дополнительные косвенные.
C-код (на основе OP):
void (*todolist[200])(void *parameters);
void *paramlist[200];
void realtime(void)
{
int i;
for (i = 0; i < 200; i++)
(*todolist[i])(paramlist[i]);
}
Код С++:
class Base {
public:
Base(void* unsafe_pointer) : unsafe_pointer_(unsafe_pointer) {}
virtual void operator()() = 0;
protected:
void* unsafe_pointer_;
};
Base* todolist[200];
void realtime() {
for (int i = 0; i < 200; ++i)
(*todolist[i])();
}
Оба скомпилированы с помощью gcc 4.8, -O3:
realtime: |_Z8realtimev:
.LFB0: |.LFB3:
.cfi_startproc | .cfi_startproc
pushq %rbx | pushq %rbx
.cfi_def_cfa_offset 16 | .cfi_def_cfa_offset 16
.cfi_offset 3, -16 | .cfi_offset 3, -16
xorl %ebx, %ebx | movl $todolist, %ebx
.p2align 4,,10 | .p2align 4,,10
.p2align 3 | .p2align 3
.L3: |.L3:
movq paramlist(%rbx), %rdi | movq (%rbx), %rdi
call *todolist(%rbx) | addq $8, %rbx
addq $8, %rbx | movq (%rdi), %rax
| call *(%rax)
cmpq $1600, %rbx | cmpq $todolist+1600, %rbx
jne .L3 | jne .L3
popq %rbx | popq %rbx
.cfi_def_cfa_offset 8 | .cfi_def_cfa_offset 8
ret | ret
В коде С++ первый movq
получает адрес vtable, а затем call
индексирует это. Так что одна служебная нагрузка.
Согласно OP, компилятор DSP С++ создает следующий код. Я вставил комментарии, основываясь на моем понимании того, что происходит (что может быть неправильно). Обратите внимание, что (IMO) цикл начинает одно местоположение раньше, чем указывает OP; в противном случае это не имеет смысла (для меня).
# Initialization.
# i3=todolist; i5=paramlist | # i5=todolist holds paramlist
i3=0xb27ba; | # No paramlist in C++
i5=0xb28e6; | i5=0xb279a;
# r15=count
r15=0xc8; | r15=0xc8;
# Loop. We need to set up r4 (first parameter) and figure out the branch address.
# In C++ by convention, the first parameter is 'this'
# Note 1:
r4=dm(i5,m6); # r4 = *paramlist++; | i5=modify(i5,m6); # i4 = *todolist++
| i4=dm(m7,i5); # ..
# Note 2:
| r2=i4; # r2 = obj
| i4=dm(m6,i4); # vtable = *(obj + 1)
| r1=dm(0x3,i4); # r1 = vtable[3]
| r4=r2+r1; # param = obj + r1
i12=dm(i3,m6); # i12 = *todolist++; | i12=dm(0x5,i4); # i12 = vtable[5]
# Boilerplate call. Set frame pointer, push return address and old frame pointer.
# The two (push) instructions after jump are actually executed before the jump.
r2=i6; | r2=i6;
i6=i7; | i6=i7;
jump (m13,i12) (db); | jump (m13,i12) (db);
dm(i7,m7)=r2; | dm(i7,m7)=r2;
dm(i7,m7)=0x1279de; | dm(i7,m7)=0x1279e2;
# if (count--) loop
r15=r15-1; | r15=r15-1;
if ne jump (pc, 0xfffffff2); | if ne jump (pc, 0xffffffe7);
Примечания:
-
В версии С++ кажется, что компилятор решил выполнить пост-инкремент в два шага, по-видимому, потому, что он хочет получить результат в регистре
i
, а не вr4
. Это, несомненно, связано с проблемой ниже. -
Компилятор решил вычислить базовый адрес реального класса объекта, используя объект vtable. Это занимает три инструкции и, по-видимому, также требует использования
i4
как временного шага 1. Сам поиск vtable занимает одну инструкцию.
Итак: проблема не в vtable lookup, что могло быть сделано в одной дополнительной инструкции (но на самом деле требуется два). Проблема в том, что компилятор чувствует необходимость "находить" объект. Но почему gcc/i86 не нужно делать?
Ответ: он привык, но это уже не так. Во многих случаях (например, если нет множественного наследования), приведение указателя на производный класс к указателю базового класса не требует модификации указателя. Следовательно, когда мы вызываем метод производного класса, мы можем просто дать ему указатель базового класса как его параметр this
. Но в других случаях это не работает, и мы должны отрегулировать указатель, когда делаем актерский состав, и, следовательно, отрегулируем его, когда мы выполняем вызов.
Есть (по крайней мере) два способа выполнить вторую настройку. Один из них показан способом сгенерированного кода DSP, где настройка сохраняется в таблице vtable, даже если она равна 0, а затем применяется во время вызова. Другим способом (называемым vtable-thunks
) является создание thunk
- немного исполняемого кода, который корректирует указатель this
, а затем переходит к точке ввода метода и помещает указатель на этот кусок в таблицу vtable. (Это может быть сделано во время компиляции.) Преимущество решения thunk заключается в том, что в обычном случае, когда корректировка не требуется, мы можем оптимизировать пробой и нет кода регулировки слева. (Недостатком является то, что если нам нужна настройка, мы создали дополнительную ветвь.)
Как я понимаю, VisualDSP ++ основан на gcc, и он может иметь опции -fvtable-thunks
и -fno-vtable-thunks
. Таким образом, вы можете скомпилировать с помощью -fvtable-thunks
. Но если вы это сделаете, вам нужно будет скомпилировать все библиотеки С++, которые вы используете с этим параметром, потому что вы не можете смешивать два стиля вызова. Кроме того, были (15 лет назад) различные ошибки в реализации gcc vtable-thunks, поэтому, если версия gcc, используемая VisualDSP ++, достаточно старая, вы можете столкнуться с этими проблемами (IIRC, все они связаны с множественным наследованием, поэтому они могут не относится к вашему случаю использования.)
(Оригинальный тест перед обновлением):
Я попробовал следующий простой случай (без множественного наследования, которое может замедлить работу):
class Base {
public:
Base(int val) : val_(val) {}
virtual int binary(int a, int b) = 0;
virtual int unary(int a) = 0;
virtual int nullary() = 0;
protected:
int val_;
};
int binary(Base* begin, Base* end, int a, int b) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->binary(a, b); }
return accum;
}
int unary(Base* begin, Base* end, int a) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->unary(a); }
return accum;
}
int nullary(Base* begin, Base* end) {
int accum = 0;
for (; begin != end; ++begin) { accum += begin->nullary(); }
return accum;
}
И скомпилировал его с помощью gcc (4.8), используя -O3. Как я и ожидал, он подготовил точно такой же код сборки, что и ваша C-отправка. Здесь цикл for
в случае функции unary
, например:
.L9:
movq (%rbx), %rax
movq %rbx, %rdi
addq $16, %rbx
movl %r13d, %esi
call *8(%rax)
addl %eax, %ebp
cmpq %rbx, %r12
jne .L9