Снижение производительности при поиске vtable в C++

Я оцениваю возможность переписать часть программного обеспечения реального времени с языка C/ассемблера на язык C++/ассемблера (по причинам, не относящимся к вопросу, части кода абсолютно необходимы для выполнения на ассемблере).

Прерывание происходит с частотой 3 кГц, и для каждого прерывания необходимо выполнить около 200 различных действий в последовательности. Процессор работает на частоте 300 МГц, что дает нам 100 000 циклов для выполнения работы. Это было решено в C с помощью массива указателей на функции:

// Each function does a different thing, all take one parameter being a pointer
// to a struct, each struct also being different.
void (*todolist[200])(void *parameters);

// Array of pointers to structs containing each function's parameters.
void *paramlist[200];

void realtime(void)
{
  int i;
  for (i = 0; i < 200; i++)
    (*todolist[i])(paramlist[i]);
}

Скорость важна. Вышеупомянутые 200 итераций выполняются 3000 раз в секунду, поэтому практически мы делаем 600 000 итераций в секунду. Приведенный выше цикл for компилируется в пять циклов на итерацию, что дает общую стоимость 3 000 000 циклов в секунду, т. е. 1% загрузки ЦП. Оптимизация на ассемблере может сократить это число до четырех инструкций, однако я боюсь, что мы можем получить дополнительную задержку из-за близкого доступа к памяти и т. д. Короче говоря, я считаю, что эти пять циклов довольно оптимальны.

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

class Base
{
  virtual void Execute();
  int something_all_things_need;
}
class Derived1 : Base
{
  void Execute() { /* Do something */ }
  int own_parameter;
  // Other own parameters
}
class Derived2 : Base { /* Etc. */ }

Base *todolist[200];

void realtime(void)
{
  for (int i = 0; i < 200; i++)
    todolist[i]->Execute(); // vtable look-up! 20+ cycles.
}

Моя проблема заключается в поиске vtable. Я не могу выполнять 600 000 запросов в секунду; это будет составлять более 4% неиспользуемой загрузки ЦП. Кроме того, список задач никогда не меняется во время выполнения, он настраивается только один раз при запуске, поэтому усилия по поиску функции для вызова действительно напрасны. Задав себе вопрос «каков наиболее оптимальный конечный результат», я смотрю на ассемблерный код, заданный решением C, и заново нахожу массив указателей на функции...

Каков чистый и правильный способ сделать это на С++? Создание хорошего базового класса, производных классов и т. д. кажется довольно бессмысленным, когда, в конце концов, вы снова выбираете указатели функций из соображений производительности.

Обновление (включая исправление начала цикла):

Процессор — ADSP-214xx, а компилятор — VisualDSP++ 5.0. При включении #pragma optimize_for_speed цикл С составляет 9 циклов. На мой взгляд, оптимизация сборки дает 4 цикла, однако я не проверял это, поэтому это не гарантируется. Цикл С++ составляет 14 циклов. Я знаю, что компилятор мог бы работать лучше, однако я не хотел сбрасывать со счетов проблему компилятора — обходиться без полиморфизма по-прежнему предпочтительнее во встроенном контексте, и выбор дизайна по-прежнему интересует меня. Для справки, вот получившаяся сборка:

C:

i3=0xb27ba;
i5=0xb28e6;
r15=0xc8;

Вот реальный цикл:

r4=dm(i5,m6);
i12=dm(i3,m6);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279de;
r15=r15-1;
if ne jump (pc, 0xfffffff2);

C++ :

i5=0xb279a;
r15=0xc8;

Вот реальный цикл:

i5=modify(i5,m6);
i4=dm(m7,i5);
r2=i4;
i4=dm(m6,i4);
r1=dm(0x3,i4);
r4=r2+r1;
i12=dm(0x5,i4);
r2=i6;
i6=i7;
jump (m13,i12) (db);
dm(i7,m7)=r2;
dm(i7,m7)=0x1279e2;
r15=r15-1;
if ne jump (pc, 0xffffffe7);

Между тем, я думаю, что нашел своего рода ответ. Наименьшее количество циклов достигается за счет наименьшего возможного. Мне нужно получить указатель данных, получить указатель функции и вызвать функцию с указателем данных в качестве параметра. При выборке указателя индексный регистр автоматически модифицируется константой, и можно с таким же успехом приравнять эту константу к 1. Таким образом, мы снова оказываемся перед массивом указателей на функции и массивом указателей на данные.

Естественно, пределом является то, что можно сделать на ассемблере, и это сейчас исследовано. Имея это в виду, я теперь понимаю, что, хотя для кого-то естественно вводить базовый класс, на самом деле это было не то, что соответствовало всем требованиям. Поэтому я предполагаю, что ответ таков: если кому-то нужен массив указателей на функции, нужно создать массив указателей на функции...


person user2711077    schedule 23.08.2013    source источник
comment
Я написал это как комментарий, но вам не кажется, что ваш «обработчик прерываний» перегружен задачами? Можете ли вы реализовать что-то вроде: обработчик прерывания + N рабочих потоков с диспетчером, поэтому, как только прибывает прерывание, вы просто «делаете заметку» о важных параметрах, которые поступают от прерывания (например, метка времени, некоторые аппаратные данные и т. д.), а затем уведомляете диспетчера, что данные поступили, поэтому ваши потоки начнут обрабатывать их параллельно или на основе определенной последовательности, которую вы можете реализовать как конечный автомат или что-то еще.   -  person evilruff    schedule 23.08.2013
comment
Идея состоит в том, что код прерывания должен оставаться «наиболее легким», насколько это возможно, чтобы быть готовым к поступающим новым данным, а обработка фактически происходит где-то еще.   -  person evilruff    schedule 23.08.2013
comment
на самом деле, вышеизложенное немного упрощено. цикл for не выполняется в прерывании, прерывание просто запускает его вне прерывания. факт остается фактом, доступно только 100.000 циклов, потом снова приходит прерывание. нагрузка на ЦП высока, независимо от того, в каком контексте выполняется код   -  person user2711077    schedule 23.08.2013
comment
У вас есть компилятор C++11?   -  person Jan Herrmann    schedule 23.08.2013
comment
@user2711077 user2711077 Можете ли вы выполнять эти задачи параллельно?   -  person evilruff    schedule 23.08.2013
comment
Избегайте VTABLES, если вы беспокоитесь о задержке, используйте шаблон с функтором T в C++, и вы избежите дополнительного поиска   -  person pyCthon    schedule 23.08.2013
comment
@evilruff Что касается процессора, то были использованы все функции, снижающие нагрузку на процессор. ЦП поддерживает параллелизм на уровне инструкций и параллелизм на уровне данных. Эффективное использование этого — вот что делает код на ассемблере. Некоторые фильтры имеют HW-ускорение, это сделано по мере возможности. Процессор более-менее загружен при нагрузке 90%+, не может быть использован намного больше. Даже если используется более мощный процессор, вышеуказанный вопрос остается прежним.   -  person user2711077    schedule 23.08.2013
comment
@JanHerrmann руководство для компилятора, поставляемого с рассматриваемым DSP, ссылается на книгу Бьярна Страуструпа 1997 года, поэтому я предполагаю, что C ++ 11 не поддерживается: S Есть ли в этом стандарте какая-нибудь интересная функция, которую вы имели в виду?   -  person user2711077    schedule 23.08.2013
comment
Есть такая вещь, как штраф за абстракцию. Если вы не можете себе этого позволить, используйте низкоуровневые конструкции или низкоуровневый язык.   -  person n. 1.8e9-where's-my-share m.    schedule 23.08.2013
comment
@user2711077 user2711077, мне действительно трудно поверить, что более мощный процессор не решит проблему накладных расходов vtable (если это проблема).   -  person user1764961    schedule 23.08.2013
comment
@user1764961 user1764961 Ну, конечно, это решает это для этого конкретного случая, но это не общая проблема, которую я здесь решаю. Предположим, более мощный процессор при загрузке 90%+. Это может быть в два или десять раз быстрее, предположим, что этот процессор загружен. Остается вопрос, каков чистый способ перебора массива указателей на функции в С++?   -  person user2711077    schedule 23.08.2013
comment
@ user2711077, можете ли вы определить «чистый»?   -  person user1764961    schedule 23.08.2013
comment
@ user2711077 какой DSP вы используете?   -  person evilruff    schedule 23.08.2013


Ответы (6)


Что заставляет вас думать, что накладные расходы на поиск в vtable составляют 20 циклов? Если это действительно так, вам нужен лучший компилятор C++.

Я попробовал это на компьютере Intel, ничего не зная о процессоре, который вы используете, и, как и ожидалось, разница между кодом отправки C и отправкой vtable C++ составляет одну инструкцию, имеющую отношение к тому факту, что 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

В коде C++ первый movq получает адрес vtable, а call затем индексирует его. Так что это одна служебная инструкция.

Согласно OP, компилятор C++ 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);

Примечания:

  1. В версии C++ кажется, что компилятор решил выполнить пост-инкремент в два этапа, по-видимому, потому, что он хочет получить результат в регистре i, а не в r4. Это, несомненно, связано с проблемой ниже.

  2. Компилятор решил вычислить базовый адрес реального класса объекта, используя виртуальную таблицу объекта. Это занимает три инструкции и, по-видимому, также требует использования i4 в качестве временного на шаге 1. Сам поиск в vtable занимает одну инструкцию.

Итак: проблема не в поиске vtable, который можно было бы сделать в одной дополнительной инструкции (но на самом деле требуется две). Проблема в том, что компилятор чувствует необходимость «найти» объект. Но почему gcc/i86 не должен этого делать?

Ответ: раньше было, но больше нет. Во многих случаях (например, при отсутствии множественного наследования) приведение указателя производного класса к указателю базового класса не требует изменения указателя. Следовательно, когда мы вызываем метод производного класса, мы можем просто передать ему указатель базового класса в качестве параметра this. Но в других случаях это не работает, и нам приходится корректировать указатель, когда мы делаем приведение, и, следовательно, корректировать его обратно, когда мы делаем вызов.

Есть (по крайней мере) два способа выполнить вторую настройку. Один из них показан сгенерированным кодом DSP, где настройка сохраняется в виртуальной таблице, даже если она равна 0, а затем применяется во время вызова. Другой способ (называемый vtable-thunks) заключается в создании thunk — небольшого исполняемого кода, — который настраивает указатель this, а затем переходит к точке входа метода и помещает указатель на этот преобразователь в виртуальную таблицу. (Все это можно сделать во время компиляции.) Преимущество решения с переходником состоит в том, что в обычном случае, когда не требуется никаких корректировок, мы можем оптимизировать преобразователь, и не остается никакого корректирующего кода. (Недостаток в том, что если нам нужна корректировка, мы создали дополнительную ветвь.)

Насколько я понимаю, VisualDSP++ основан на gcc, и у него могут быть опции -fvtable-thunks и -fno-vtable-thunks. Таким образом, вы можете скомпилировать с помощью -fvtable-thunks. Но если вы это сделаете, вам нужно будет скомпилировать все библиотеки C++, которые вы используете с этой опцией, потому что вы не можете смешивать два стиля вызова. Кроме того, (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
person rici    schedule 23.08.2013
comment
у меня другой процессор, на самом деле это DSP, оптимизированный для 32-битной плавающей запятой. там нет выполнения не по порядку, я думаю, что вряд ли есть даже прогнозирование ветвлений, поэтому от ЦП не следует ожидать большой динамической помощи. насчет количества циклов, честно говоря, я немного преувеличил, это скорее 10+ инструкций. факт остается фактом: поиск в vtable выполняется для каждой итерации, что приводит к существенным штрафам. - person user2711077; 23.08.2013
comment
@ user2711077: Я предполагал, что у вас другой процессор, поэтому у меня нет возможности проверить. Но i386 не использует здесь большого преимущества внеочередного выполнения: в приведенном выше цикле единственным реальным преимуществом является увеличение %rbx перед вызовом. Разница между этим и отправкой C, по сути, состоит в одной инструкции, первой movq, потому что ей нужно сделать одну дополнительную косвенность. Может ли одно косвенное обращение стоить 10 циклов на вашем DSP? - person rici; 23.08.2013
comment
@ user2711077: Хорошо, изменил пример точно на ваш код. Как эти два компилируются в вашей архитектуре с использованием соответствующих настроек оптимизации? - person rici; 23.08.2013
comment
@ user2711077: На самом деле, учитывая ASM в этом ответе и тот факт, что вы говорите, что ваш компилятор не очень хорош, вы можете вручную развернуть свой цикл в дополнение к любому другому ответу, который вы найдете. Ручное разворачивание узких петель может иметь небольшое значение. - person Mooing Duck; 23.08.2013
comment
@rici, хотя я бы предпочел шаблонный подход, я проголосовал за ваш ответ, поскольку он точно показывает, что компилятор сделал с кодом. доступ к vtable действительно быстрый, с приличным компилятором. почему я предпочитаю шаблонный подход, так это то, что я не вижу необходимости, особенно во встроенных системах, использовать полиморфизм... (также и для других областей, но это другое обсуждение). - person Stefan; 23.08.2013
comment
@MooingDuck: Хороший вопрос. Компиляция с -funroll-loops превратила цикл в 8 повторений movq/movq/callq (C++), что, вероятно, является победой. - person rici; 23.08.2013
comment
Хороший анализ у вас. У вас есть очень веская точка зрения в том, что компилятор не выполняет свою работу наилучшим образом (см. Обновление выше). Я голосую за ваш ответ, однако я больше искал альтернативные способы ведения дел. Просто отклонить это как проблему компилятора и продолжать не хотелось до конца исследовать проблему и ее различные решения. - person user2711077; 26.08.2013
comment
@user2711077. Спасибо. Я просмотрел ваше редактирование и соответствующим образом отредактировал свой пост; Я думаю, что стоимость, которую вы видите, - это настройка указателя, а не поиск в vtable, хотя, по общему признанию, разница несколько тонкая. Тем не менее, всегда полезно иметь четкое представление о том, что происходит. - person rici; 27.08.2013
comment
На упорядоченных процессорах пузырек конвейера из-за неправильного предсказания ветвления абсолютно может вызвать задержку в 20 циклов. Я измерил это напрямую: assemblyrequired.crashworks. org/2009/01/19/ Этого следует ожидать, когда этап разрешения ветвей конвейера на 20 этапов опережает выборку инструкций. - person Crashworks; 27.08.2013
comment
@Crashworks: да, но: (1) мы не сравниваем вызов виртуальной функции с прямым вызовом функции. Мы сравниваем его с косвенным вызовом функции, хотя и с косвенностью на один уровень меньше. Существует точно такой же штраф за неправильное предсказание, потому что по-прежнему имеет место случай, когда один и тот же сайт вызова разветвляется в разные, непредсказуемые местоположения. И (2): DSP не имеет предсказания переходов. Однако у него есть два слота задержки после перехода, и оба они используются операциями корректировки стека перед вызовом, поэтому задержка ветвления используется полностью. - person rici; 27.08.2013
comment
так что для тех, кто не хочет читать все сообщение - какой вывод? на современном ПК каковы средние накладные расходы на поиск vtbl в наносекундах? - person Oleg Vazhnev; 03.04.2015
comment
@javapowered: версия tl; dr: где-то между ничем и почти ничем. Вы не заметите разницы. - person rici; 04.04.2015

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

template <typename FirstCb, typename ... RestCb>
struct InterruptHandler {
    void execute() {
        // I construct temporary objects here since I could not figure out how you
        // construct your objects. You can change these signatures to allow for 
        // passing arbitrary params to these handlers.
        FirstCb().execute();
        InterruptHandler<RestCb...>().execute();
    }
}

InterruptHandler</* Base, Derived1, and so on */> handler;

void realtime(void) {
    handler.execute();
}

Это должно полностью исключить поиск в vtable, предоставляя больше возможностей для оптимизации компилятора, поскольку код внутри execute может быть встроен.

Однако обратите внимание, что вам нужно будет изменить некоторые части в зависимости от того, как вы инициализируете свои обработчики. Базовая структура должна остаться прежней. Кроме того, для этого требуется компилятор, совместимый с C++11.

person Ajit Singh    schedule 23.08.2013

Я предлагаю использовать статические методы в ваших производных классах и помещать эти функции в ваш массив. Это устранило бы накладные расходы на поиск в v-таблице. Это ближе всего к вашей реализации на языке C.

В конечном итоге вы можете пожертвовать полиморфизмом ради скорости.
Необходимо ли наследование?
Тот факт, что вы переходите на C++, не означает, что вы должны переключиться на объектно-ориентированный.

Кроме того, пробовали ли вы развернуть свой цикл в ISR?
Например, выполните 2 или более вызовов выполнения, прежде чем вернуться к началу цикла.

Кроме того, можете ли вы перенести какую-либо функциональность из ISR? Может ли какая-то часть функционала выполняться "фоновым циклом" вместо ISR? Это уменьшит время в вашем ISR.

person Thomas Matthews    schedule 23.08.2013
comment
Yay для простого быстрого решения. Тот факт, что у вас есть компилятор C++, не означает, что вам нужно использовать все эти блестящие навороты. - person johnwbyrd; 25.12.2013

Вы можете скрыть стирание типа void* и восстановить тип внутри шаблонов. Результатом будет (надеюсь) тот же массив для указателей на функции. Эта помощь с приведением и совместимостью с вашим кодом:

#include <iostream>

template<class ParamType,class F>
void fun(void* param) {
  F f;
  f(*static_cast<ParamType*>(param));
}

struct my_function {
  void operator()(int& i) {
    std::cout << "got it " << i << std::endl;
  }
};


int main() {
  void (*func)(void*) = fun<int, my_function>;

  int j=4;
  func(&j);

  return 0;
}

В этом случае вы можете создавать новые функции как объект функции с более безопасным типом. «Обычный» подход ООП с виртуальными функциями здесь не помогает.

В случае среды C++11 вы можете создать массив с помощью вариативных шаблонов во время компиляции (но со сложным синтаксисом).

person Jan Herrmann    schedule 23.08.2013

Это не связано с вашим вопросом, но если вы заинтересованы в производительности, вы можете использовать шаблоны для развертывания цикла для списка задач:

void (*todo[3])(void *);
void *param[3];

void f1(void*) {std::cout<<"1" << std::endl;}
void f2(void*) {std::cout<<"2" << std::endl;}
void f3(void*) {std::cout<<"3" << std::endl;}

template<int N>
struct Obj {
    static void apply()
    {
        todo[N-1](param[N-1]);
        Obj<N-1>::apply();
    }
};

template<> struct Obj<0> { static void apply() {} };

todo[0] = f1;
todo[1] = f2;
todo[2] = f3;

Obj<sizeof todo / sizeof *todo>::apply();
person Serve Laurijssen    schedule 27.08.2013

Узнайте, куда ваш компилятор помещает виртуальную таблицу, и получите к ней прямой доступ, чтобы получить указатели на функции и сохранить их для использования. Таким образом, у вас будет почти тот же подход, что и в C, с массивом указателей на функции.

person user1764961    schedule 23.08.2013
comment
Дорогой Бог, пожалуйста, не позволь таким вещам происходить на Твоей зеленой земле. Спасибо. - person n. 1.8e9-where's-my-share m.; 23.08.2013
comment
Очень опасно, если производители компилятора или версии компилятора меняются. - person Thomas Matthews; 23.08.2013
comment
@ Томас, это совсем не опасно. Если вы делаете это, вы знаете, о чем вам нужно позаботиться. Как только объект создан, виртуальная таблица заполняется значениями и не меняется, как и его местоположение. Если вы бреетесь бритвой, да, вы можете порезаться, если вы неуклюжи, но вы также можете использовать электробритву. - person user1764961; 23.08.2013
comment
Балансировать машину на этом вертикальном столбе нормально, если не облажаться. - person Mooing Duck; 23.08.2013
comment
@user1764961: user1764961: То, что ваш исполняемый файл не меняется, не означает, что путь кода тот же. Библиотеки и DLL постоянно обновляются. Кроме того, если изменится компилятор, или вы измените параметр компилятора, или он скомпилирован в другое время дня, виртуальная таблица может измениться, что приведет к сбою всей вашей тяжелой работы. - person Mooing Duck; 24.08.2013
comment
1. вы не меняете компилятор 15 раз в день. 2. Даже когда вы это сделаете, вы сможете увидеть, было ли изменено местоположение. 3. Если вы только что обновили текущий компилятор, vtbl, скорее всего, будет на том же месте. Например, MS десятилетиями не меняла местоположение vtbl. Если бы вы писали компилятор, реализовали бы вы перемещение vtbl в каждой сборке, как вы предлагаете? Я так не думаю. Итак, перейдем от всех теоретических возможностей к практике. - person user1764961; 24.08.2013
comment
Не забывайте, что это встроенный мир. Код здесь редко меняется, а компилятор c/c++ воспринимается скорее как продвинутый ассемблер. Это компромисс, на который вам, возможно, придется пойти, чтобы обойти плохо оптимизирующий компилятор С++. Я не говорю, что это правильный путь, но это определенно кандидат. - person Agoston Horvath; 08.07.2014
comment
Хотя это не сработает, если ваш LLD определяет ваш компилятор как все вещи, для тех, кто работает над оптимизацией драйверов устройств для четко определенной платформы, я не буду проблемой. __asm__ существует в большинстве компиляторов C++ не просто так. - person c z; 06.08.2020