Telegram Group & Telegram Channel
#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 с неинстанциированными типами при этом приведет к ошибке линковки.)



group-telegram.com/alisa_rummages/206
Create:
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

View MORE
Open in Telegram


Telegram | DID YOU KNOW?

Date: |

"The result is on this photo: fiery 'greetings' to the invaders," the Security Service of Ukraine wrote alongside a photo showing several military vehicles among plumes of black smoke. So, uh, whenever I hear about Telegram, it’s always in relation to something bad. What gives? Andrey, a Russian entrepreneur living in Brazil who, fearing retaliation, asked that NPR not use his last name, said Telegram has become one of the few places Russians can access independent news about the war. "He has to start being more proactive and to find a real solution to this situation, not stay in standby without interfering. It's a very irresponsible position from the owner of Telegram," she said. "Your messages about the movement of the enemy through the official chatbot … bring new trophies every day," the government agency tweeted.
from id


Telegram Алиса копается
FROM American