处理用于创建算法的多个版本的 #ifdef
Handling #ifdef's that were used to create multiple versions of an algorithm
我正在尝试对用C++编写的算法的许多(大约25种(变体进行基准测试。
我使用三种方法的组合实现了这些变体:
-
复制代码并对复制的版本进行微小更改
-
对基本算法类进行子类划分
-
使用
#ifdef
s在代码片段之间切换
选项1和2产生的变化是可以的,因为我可以选择在配置文件中运行算法的哪个变化。然后,我可以遍历不同的配置文件,并记录"configuration:results"对——保持这些记录对我的工作非常重要。
我目前对#ifdef
有一个问题,因为我必须编译多个版本的代码才能访问这些变体,这使得运行自动化实验脚本和准确记录结果变得更加困难。然而,#ifdef
非常有用,因为如果我在一个代码副本中发现错误,那么我不必记住在多个副本中纠正这个错误。
#ifdef
s将我通过复制代码和子类化创建的六个变体扩展为24个总变体(每个基本变体有4个变体(
这里有一个例子,主要是我使用#ifdef
来避免复制太多代码:
....
double lasso_gam=*gamma;
*lasso_idx=-1;
for(int aj=0;aj<(int)a_idx.size();aj++){
int j=a_idx[aj];
assert(j<=C*L);
double inc=wa[aj]*(*gamma)*signs[aj];
if( (beta_sp(j)>0 && beta_sp(j)+inc<0)
#ifdef ALLOW_NEG_LARS
|| (beta_sp(j)<0 && beta_sp(j)+inc>0)
#else
|| (beta_sp(j)==0 && beta_sp(j)+inc<0)
#endif
){
double tmp_gam=-beta_sp(j)/wa[aj]*signs[aj];
if(tmp_gam>=0 && tmp_gam<lasso_gam) {
*lasso_idx=aj;
*next_active=j;
lasso_gam=tmp_gam;
}
}
}
if(lasso_idx>=0){
*gamma=lasso_gam;
}
....
问题:如果配置文件指定了要运行的算法变体,那么允许运行当前由#ifdef
s指定的算法变体的最佳方式是什么。
理想情况下,我只想编译一次代码,并在运行时使用配置文件选择一个算法变体。
您可以使用(可能是额外的(模板参数来增强您的算法,如下所示:
enum class algorithm_type
{
type_a,
type_b,
type_c
};
template <algorithm_type AlgorithmType>
void foo(int usual, double args)
{
std::cout << "common code" << std::endl;
if (AlgorithmType == algorithm_type::type_a)
{
std::cout << "doing type a..." << usual << ", " << args << std::endl;
}
else if (AlgorithmType == algorithm_type::type_b)
{
std::cout << "doing type b..." << usual << ", " << args << std::endl;
}
else if (AlgorithmType == algorithm_type::type_c)
{
std::cout << "doing type c..." << usual << ", " << args << std::endl;
}
std::cout << "more common code" << std::endl;
}
现在你可以通过这个模板参数选择你的行为:
foo<algorithm_type::type_a>(11, 0.1605);
foo<algorithm_type::type_b>(11, 0.1605);
foo<algorithm_type::type_c>(11, 0.1605);
该类型是一个常量表达式,产生一个编译时决定的分支(也就是说,其他分支已知为死代码并已删除(。事实上,编译器应该对此发出警告(如何处理取决于您(。
但您仍然可以很好地调度运行时值:
#include <stdexcept>
void foo_with_runtime_switch(algorithm_type algorithmType,
int usual, double args)
{
switch (algorithmType)
{
case algorithm_type::type_a:
return foo<algorithm_type::type_a>(usual, args);
case algorithm_type::type_b:
return foo<algorithm_type::type_b>(usual, args);
case algorithm_type::type_c:
return foo<algorithm_type::type_c>(usual, args);
default:
throw std::runtime_error("wat");
}
}
foo_with_runtime_switch(algorithm_type::type_a, 11, 0.1605);
foo_with_runtime_switch(algorithm_type::type_b, 11, 0.1605);
foo_with_runtime_switch(algorithm_type::type_c, 11, 0.1605);
算法的内部保持不变(消除了死分支,进行了相同的优化(,只是实现方法发生了变化。(请注意,可以推广枚举思想,以便自动生成此开关;如果你发现自己有很多变体,这可能是很好的学习方法。(
当然,你仍然可以默认#define
一个特定的算法:
#define FOO_ALGORITHM algorithm_type::type_a
void foo_with_define(int usual, double args)
{
return foo<FOO_ALGORITHM>(usual, args);
}
foo_with_define(11, 0.1605);
所有这些加在一起会给你带来三者的优势,不会重复。
在实践中,您可以将这三个都作为重载:一个用于那些知道在编译时使用哪种算法的用户,那些需要在运行时选择它的用户,以及那些只想要默认值(您可以通过项目范围的#define
覆盖它(的用户:
// foo.hpp
enum class algorithm_type
{
type_a,
type_b,
type_c
};
// for those who know which algorithm to use
template <algorithm_type AlgorithmType>
void foo(int usual, double args)
{
std::cout << "common code" << std::endl;
if (AlgorithmType == algorithm_type::type_a)
{
std::cout << "doing type a..." << usual << ", " << args << std::endl;
}
else if (AlgorithmType == algorithm_type::type_b)
{
std::cout << "doing type b..." << usual << ", " << args << std::endl;
}
else if (AlgorithmType == algorithm_type::type_c)
{
std::cout << "doing type c..." << usual << ", " << args << std::endl;
}
std::cout << "more common code" << std::endl;
}
// for those who will know at runtime
void foo(algorithm_type algorithmType, int usual, double args)
{
switch (algorithmType)
{
case algorithm_type::type_a:
return foo<algorithm_type::type_a>(usual, args);
case algorithm_type::type_b:
return foo<algorithm_type::type_b>(usual, args);
case algorithm_type::type_c:
return foo<algorithm_type::type_c>(usual, args);
default:
throw std::runtime_error("wat");
}
}
#ifndef FOO_ALGORITHM
// chosen to be the best default by profiling
#define FOO_ALGORITHM algorithm_type::type_b
#endif
// for those who just want a good default
void foo(int usual, double args)
{
return foo<FOO_ALGORITHM>(usual, args);
}
当然,如果某些实现类型总是比其他实现类型差,那么就去掉它。但是,如果你发现有两种有用的实现,那么以这种方式同时使用这两种实现也没有害处。
如果您有多个版本的#ifdef
,通常最好构建多个可执行文件,并让您的配置脚本决定在基准测试时运行哪些可执行文件。然后在Makefile中有规则来构建各种配置:
%-FOO.o: %.cc
$(CXX) -c $(CFLAGS) -DFOO -o $@ $<
%-BAR.o: %.cc
$(CXX) -c $(CFLAGS) -DBAR -o $@ $<
test-FOO: $(SRCS:%.cc=%-FOO.o)
$(CXX) $(LDFLAGS) -DFOO -o $@ $^ $(LDLIBS)
如果你的#if
分散在各处,并在这里或那里更改了一行代码,那么根据传递到要运行其变体的函数中的枚举,将所有#if
转换为if
,并希望编译器在优化方面做得很好。希望它能生成与多次定义函数几乎相同的代码,只是使用一个运行时条件来决定运行哪个。没有承诺。
如果您正在#if
中处理算法中的代码块,则将算法拆分为更小的函数,整个算法的不同实现可以调用这些函数。如果#if
的侵入性太强,最终会有50个功能,那么这显然是不切实际的。
您还没有提到您使用的编译器,但您可以在命令行上为其中任何一个设置#defines。在gcc中,您只需要添加-D MYTESTFOO
来定义MYTESTFOO。这将使#定义前进的道路——没有代码更改要传播,当然,每个测试都有不同的编译代码,但它应该很容易自动化。
如果将算法本身放置在具有相同接口的类中,则可以将它们作为模板参数传递到使用算法的位置。
class foo {
public:
void do_something() {
std::cout << "foo!" << std::endl;
}
}
class bar {
public:
void do_something() {
std::cout << "bar!" << std::endl;
}
template <class meh>
void something() {
meh algorithm;
meh.do_something();
}
int main() {
std::vector<std::string> config_values = get_config_values_from_somewhere();
for (const austo& config : config_values) { // c++11 for short notation
switch (config) {
case "foo":
something<foo>();
break;
case "bar":
something<bar>();
break;
default:
std::cout << "undefined behaviour" << std::endl;
}
}
}
这样你就可以同时使用不同的行为,并通过它们的名字来区分它们。此外,如果您不使用其中一个,优化器将在编译时删除它(但这不是您的问题(。
在读取配置文件时,您只需要一个工厂(或类似的工厂(来创建对象/函数的正确实例,该实例应在使用算法之前使用该算法。
编辑:添加了基本开关。
一种方法是不在可执行文件中包含预处理器指令,而是这样做:
#define METHOD METHOD1
int Method1() { return whatever(); };
#undef METHOD
#define METHOD METHOD2
int Method2() { return whatever(); };
#undef METHOD
假设CCD_ 16依赖于CCD_ 17,那么这些将给出不同的结果。
- 为cl.exe(Visual Studio代码)指定命令行C++版本
- 导入库可以跨dll版本工作吗
- #ifdef和未声明的标识符
- 在调用FreeLibrary后,释放动态链接到具有相同版本的CRT堆的DLL的内存
- 在clang++预处理器中确定gcc工具链版本
- 码头化的C++应用程序是否向后兼容早期的内核版本
- C++ Macros #ifdef
- 不同的Visual Studio版本中缺少.dll
- 用符号版本替换对函数的所有调用
- luaL_dofile在已知良好的字节码上失败,可以使用未编译的版本
- 正在解码MSVC 32位版本的程序集(作业).没有手术做什么
- 我需要分发哪些版本的可再分发文件
- CV_OCL_RUN宏如何在OpenCV(版本3.4.5)的goodFeaturesToTrack实现中工作?
- 在运行时读取 libstdc++ 版本
- 如何声明一个可以在整个程序中使用的全局 2d 3d 4d .. 数组(堆版本)变量?
- FindPackageHandleStandardArgs.cmake:137 的 CMake 错误(消息):找不到 Boost (缺少:正则表达式)(找到合适的版本"1.72.0",
- Vulkan SDK 版本 1.1.85.0 在 Kubuntu 18.10 上链接
- 如何正确实现与基类不同的版本?
- 有没有办法在不使用 #ifdef 的情况下不编译发布版本中的单元测试函数体?
- 处理用于创建算法的多个版本的 #ifdef