group-telegram.com/alisa_rummages/206
Create:
Last Update:
Last Update:
#dev #cpp #rust
Есть баян, что вот в таком коде функция в качестве коллбека менее эффективна, чем лямбда:
#include <cstdio>
__attribute__((noinline))
void invoke_callback(auto callback) {
callback();
}
void my_callback() {
puts("invoke via function");
}
int main() {
invoke_callback(my_callback);
invoke_callback([]() {
puts("invoke via lambda");
});
}
Ларчик открывается просто:
my_callack
мгновенно превращается в указатель на функцию, и invoke_callback
приходится делать непрямой вызов по указателю. Лямбда же является уникальным типом с фиксированным перегруженным operator()
, поэтому коллбек спокойно инлайнится. Не менее баян, что в Rust это не проблема, и#[inline(never)]
fn invoke_callback(callback: impl FnOnce()) {
callback();
}
fn my_callback() {
println!("invoke via function");
}
#[no_mangle]
fn main() {
invoke_callback(my_callback);
invoke_callback(|| {
println!("invoke via lambda");
});
}
работает эффективно в обоих случаях, ведь в Rust функции являются не указателями, а анонимными ZST-типами, прям как лямбды. А вот вам не баян: оказывается, если заменить вызов
invoke_callback
на invoke_callback(my_callback as fn())
, чтобы произошел вызов по указателю, производительность не ухудшится! Почему?Здесь происходит нечто в духе девиртуализации. При интер-процедурном анализе компилятор видит, что аргумент
invoke_callback<fn()>
— это всегда указатель на my_callback
, поэтому callback()
можно оптимизировать до my_callback()
.Почему Clang не может сделать то же самое, ведь он тоже использует LLVM? Может, если
invoke_callback
сделать статической функцией (GCC тоже делает девиртуализацию, но потом не инлайнит вызов). Функции без static
же видны из других единицах трансляции, поэтому вполне возможно, что кто-нибудь "снаружи" вызовет invoke_callback<void (*)()>
с аргументом помимо my_callback
, и в этом случае оптимизация неприменима.Естественно, как только начинают использоваться два разных коллбека, девиртуализация ломается, так что использовать ZST-типы вместо указателей на функции все еще полезно.
Забавно, что даже то, что
invoke_callback
— шаблонная функция, не помогает, хотя, казалось бы, раз каждый cpp
-файл #include
'ит в себя заголовок с реализацией шаблонной функции, то проводить оптимизацию в рамках единицы трансляции корректно. Это не так по двум причинам.Во-первых, см. пост выше:
inline
-функции пусть и компилируются несколько раз, но потом дедублицируются, т.е. код, сгенерированной каждой единицей трансляции, должен быть валиден для вызовов из других единиц трансляции.Во-вторых, шаблонная функция может быть определена в одной единице трансляции, а затем использоваться в другой без реализации:
// a.cpp
#include <iostream>
template<typename T>
void print(T value) {
std::cout << value;
}
template void print<int>(int value);
template void print<const char*>(const char* value);
// main.cpp
template<typename T>
void print(T value);
int main() {
print(2);
print(" + ");
print(2);
print(" = ");
print(4);
print("\n");
}
Конструкция с
template
без угловых скобок инстанциирует шаблонную функцию для данного T
, и другие единицы линковки потом могут к этим инстансам линковаться. (Попытка использовать в main.cpp
print
с неинстанциированными типами при этом приведет к ошибке линковки.)BY Алиса копается
Warning: Undefined variable $i in /var/www/group-telegram/post.php on line 260
Share with your friend now:
group-telegram.com/alisa_rummages/206