Видите ли, в ситуациях, которые действительно имеют значение с точки зрения производительности, например, при многократном вызове функции за цикл, производительность может не отличаться вовсе.
Это может звучать странно для людей, которые привыкли думать о коде C как о чем-то, выполняемом абстрактной машиной C, чей «машинный язык» близко отражает сам язык C. В таком контексте «по умолчанию» косвенный вызов функции действительно медленнее, чем прямой, потому что формально он предусматривает дополнительный доступ к памяти для определения цели вызова.
Однако в реальной жизни код выполняется реальной машиной и компилируется оптимизирующим компилятором, который достаточно хорошо знает архитектуру базовой машины, что помогает ему генерировать наиболее оптимальный код для этой конкретной машины. И на многих платформах может оказаться, что наиболее эффективный способ выполнения вызова функции из цикла на самом деле приводит к идентичному коду как для прямого, так и косвенного вызова, что приводит к одинаковой производительности обоих.
Рассмотрим, например, платформу x86. Если мы «буквально» переведем прямой и косвенный вызов в машинный код, мы можем получить что-то вроде этого
// Direct call
do-it-many-times
call 0x12345678
// Indirect call
do-it-many-times
call dword ptr [0x67890ABC]
Первый использует непосредственный операнд в машинной инструкции и действительно обычно быстрее, чем последний, который должен считывать данные из некоторой независимой ячейки памяти.
Теперь давайте вспомним, что архитектура x86 на самом деле имеет еще один способ предоставления операнда для инструкции call
. Он предоставляет целевой адрес в регистре . И очень важным моментом в этом формате является то, что он обычно быстрее, чем оба указанных выше . Что это значит для нас? Это означает, что хороший оптимизирующий компилятор должен и воспользуется этим фактом. Для реализации вышеуказанного цикла компилятор попытается использовать вызов через регистр в обоих случаях. В случае успеха окончательный код может выглядеть следующим образом
// Direct call
mov eax, 0x12345678
do-it-many-times
call eax
// Indirect call
mov eax, dword ptr [0x67890ABC]
do-it-many-times
call eax
Обратите внимание, что теперь важная часть - фактический вызов в теле цикла - точно и точно одинакова в обоих случаях. Излишне говорить, что производительность будет практически идентичной .
Можно даже сказать, как бы странно это ни звучало, что на этой платформе прямой вызов (вызов с непосредственным операндом в call
) на медленнее , чем косвенный вызов до тех пор, пока операнд косвенного вызова подается в регистр (в отличие от сохранения в памяти).
Конечно, в общем случае все не так просто. Компилятору приходится иметь дело с ограниченной доступностью регистров, проблемами наложения имен и т. Д. Но в таких упрощенных случаях, как в вашем примере (и даже в гораздо более сложных), приведенная выше оптимизация будет выполняться хорошим компилятором и полностью устранит любая разница в производительности между циклическим прямым вызовом и циклическим косвенным вызовом. Эта оптимизация особенно хорошо работает в C ++ при вызове виртуальной функции, поскольку в типичной реализации задействованные указатели полностью контролируются компилятором, что дает ему полное представление о картине псевдонимов и других важных вещах.
Конечно, всегда возникает вопрос, достаточно ли умен ваш компилятор для оптимизации подобных вещей ...