如何避免对编译后无法访问的正在运行的代码部分进行运行时检查?

How to avoid run-time checks for running parts of code that become unreachable after compilation?

本文关键字:代码部 运行 检查 运行时 编译 何避免 访问      更新时间:2023-10-16

我的程序从用户那里获取几个布尔变量,之后它们的值不会改变。每个布尔变量启用一部分代码。像这样:

#include <iostream>
void callback_function(bool task_1, bool task_2, bool task_3) {
if (task_1) {
std::cout << "Running task 1" << std::endl;
}
if (task_2) {
std::cout << "Running task 2" << std::endl;
}
if (task_3) {
std::cout << "Running task 3" << std::endl;
}
}
int main() {
bool task_1 = true;
bool task_2 = false;
bool task_3 = true;
while (true) {
callback_function(task_1, task_2, task_3);
}
return 0;
}

现在我的问题是,由于布尔变量在每次程序调用callback_function()时都是固定的,有没有办法避免回调函数中的if语句?

这是避免运行时检查的一种方法(为布尔变量的所有排列实现回调函数---下面只显示两种情况):

#include <functional>
#include <iostream>
void callback_function_for_tasks_1_2_3() {
std::cout << "Running task 1" << std::endl;
std::cout << "Running task 2" << std::endl;
std::cout << "Running task 3" << std::endl;
}
void callback_function_for_tasks_1_3() {
std::cout << "Running task 1" << std::endl;
std::cout << "Running task 3" << std::endl;
}
int main() {
bool task_1 = true;
bool task_2 = false;
bool task_3 = true;
std::function<void()> callback_function;
if (task_1 && task_2 && task_3) {
callback_function = callback_function_for_tasks_1_2_3;
} else if (task_1 && !task_2 && task_3) {
callback_function = callback_function_for_tasks_1_3;
}
while (true) {
callback_function();
}
return 0;
}

问题是,如果有n布尔变量,我必须实现2^n不同的回调函数。有没有更好的方法来实现这一目标?

确保在编译时计算 if 语句

C++17 引入了if constexpr,它正是这样做的:

template<bool task_1, bool task_2, bool task_3>
void callback_function() {
if constexpr (task_1) {
std::cout << "Running task 1" << std::endl;
}
if constexpr (task_2) {
std::cout << "Running task 2" << std::endl;
}
if constexpr (task_3) {
std::cout << "Running task 3" << std::endl;
}
}

如果启用了优化,则无需if constexpr即使您使用常规if而不是if constexpr,因为布尔值现在是模板化的,编译器将能够完全消除if语句,只运行任务。如果你看一下这里生成的程序集,你会发现即使在-O1,在任何callback函数中都没有if语句。

我们现在可以直接将callback_function用作函数指针,避免function<void()>

int main() {
using callback_t = void(*)();
callback_t func = callback_function<true, false, true>;
// Do stuff with func 
}

我们还可以通过将它们分配给 constexpr 变量来命名bools:

int main() {
using callback_t = void(*)();
constexpr bool do_task1 = true;
constexpr bool do_task2 = false;
constexpr bool do_task3 = true; 
callback_t func = callback_function<do_task1, do_task2, do_task3>;
// Do stuff with func 
}

自动创建所有可能的回调函数的查找表

您提到了在运行时在不同的回调函数之间进行选择。我们可以通过查找表轻松做到这一点,并且我们可以使用模板自动创建所有可能的回调函数的查找表。

第一步是从特定索引获取回调函数:

// void(*)() is ugly to type, so I alias it
using callback_t = void(*)();
// Unpacks the bits 
template<size_t index>
constexpr auto getCallbackFromIndex() -> callback_t 
{
constexpr bool do_task1 = (index & 4) != 0;
constexpr bool do_task2 = (index & 2) != 0;
constexpr bool do_task3 = (index & 1) != 0; 
return callback_function<do_task1, do_task2, do_task3>; 
}

一旦我们可以做到这一点,我们就可以编写一个函数来从一堆索引创建一个查找表。我们的查找表将只是一个std::array

// Create a std::array based on a list of flags
// See https://en.cppreference.com/w/cpp/utility/integer_sequence
// For more information
template<size_t... Indexes>
constexpr auto getVersionLookup(std::index_sequence<Indexes...>) 
-> std::array<callback_t, sizeof...(Indexes)>
{
return {getCallbackFromIndex<Indexes>()...}; 
}
// Makes a lookup table containing all 8 possible callback functions
constexpr auto callbackLookupTable = 
getVersionLookup(std::make_index_sequence<8>()); 

在这里,callbackLookupTable包含所有 8 个可能的回调函数,其中callbackLookupTable[i]扩展i位以获取回调。例如,如果i == 6,则i的位以二进制110,因此

callbackLookupTable[6]callback_function<true, true, false>

在运行时使用查找表

使用查找表非常简单。我们可以通过位移从一堆bool中获取索引:

callback_t getCallbackBasedOnTasks(bool task1, bool task2, bool task3) {
// Get the index based on bit shifting
int index = ((int)task1 << 2) + ((int)task2 << 1) + ((int)task3); 
// return the correct callback
return callbackLookupTable[index]; 
}

演示如何在任务中读取的示例

我们现在可以在运行时获取bool,只需调用getCallbackBasedOnTasks即可获得正确的回调

int main() {
bool t1, t2, t3;
// Read in bools
std::cin >> t1 >> t2 >> t3; 
// Get the callback
callback_t func = getCallbackBasedOnTasks(t1, t2, t3); 
// Invoke the callback
func(); 
}

保持代码原样。

与写入 std::out 相比,"if"的执行时间几乎为零,所以你什么都不争论。好吧,除非您花一些时间按原样测量执行时间,并根据三个常量的值删除 if,并发现存在真正的差异。

最多,您可能会使函数内联或静态,并且编译器可能会意识到在启用优化时参数始终相同。(我的编译器会发出警告,指出您正在使用没有原型的函数,这意味着您应该将原型放入头文件中,告诉编译器期望来自其他调用站点的调用,或者您应该将其设置为静态,告诉编译器它知道所有调用,并且可以使用静态分析进行优化)。

你认为是常数的东西,可能不会永远保持不变。原始代码将起作用。任何新代码很可能都不会。

没有 JIT 编译,你不能比你的 2^n 函数(以及由此产生的二进制大小)做得更好。 当然,您可以使用模板来避免将它们全部写出来。 为了防止源仅通过选择正确的实现而呈指数级扩展,您可以编写一个递归调度程序(演示):

template<bool... BB>
auto g() {return f<BB...>;}
template<bool... BB,class... TT>
auto g(bool b,TT... tt)
{return b ? g<BB...,true>(tt...) : g<BB...,false>(tt...);}