使用函子提供函数或运算符作为C++模板参数的性能损失
Performance penalty of using functor to provide a function or an operator as a C++ template parameter?
我有一系列复杂的函数,执行非常相似的任务,除了函数中间的一个运算符。我的代码的简化版本可能是这样的:
#include <assert.h>
static void memopXor(char * buffer1, char * buffer2, char * res, unsigned n){
for (unsigned x = 0 ; x < n ; x++){
res[x] = buffer1[x] ^ buffer2[x];
}
};
static void memopPlus(char * buffer1, char * buffer2, char * res, unsigned n){
for (unsigned x = 0 ; x < n ; x++){
res[x] = buffer1[x] + buffer2[x];
}
};
static void memopMul(char * buffer1, char * buffer2, char * res, unsigned n){
for (unsigned x = 0 ; x < n ; x++){
res[x] = buffer1[x] * buffer2[x];
}
};
int main(int argc, char ** argv){
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
char res1[5] = {};
memopXor(b1, b2, res1, 5);
assert(res1[0] == 0);
assert(res1[1] == 0);
assert(res1[2] == 0);
assert(res1[3] == 0);
assert(res1[4] == 1);
char res2[5] = {};
memopPlus(b1, b2, res2, 5);
assert(res2[0] == 0);
assert(res2[1] == 2);
assert(res2[2] == 4);
assert(res2[3] == 6);
assert(res2[4] == 8);
char res3[5] = {};
memopMul(b1, b2, res3, 5);
assert(res3[0] == 0);
assert(res3[1] == 1);
assert(res3[2] == 4);
assert(res3[3] == 9);
assert(res3[4] == 16);
}
使用C++模板来避免重复代码似乎是一个很好的例子,因此我正在寻找一种方法来将我的代码更改为以下内容(伪代码):
#include <assert.h>
template <FUNCTION>
void memop<FUNCTION>(char * buffer1, char * buffer2, char * res, size_t n){
for (size_t x = 0 ; x < n ; x++){
res[x] = FUNCTION(buffer1[x], buffer2[x]);
}
}
int main(int argc, char ** argv){
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
char res1[5] = {};
memop<operator^>(b1, b2, res1, 5);
assert(res1[0] == 0);
assert(res1[1] == 0);
assert(res1[2] == 0);
assert(res1[3] == 0);
assert(res1[4] == 0);
char res2[5] = {};
memop<operator+>(b1, b2, res2, 5);
assert(res2[0] == 0);
assert(res2[1] == 2);
assert(res2[2] == 4);
assert(res2[3] == 6);
assert(res2[4] == 8);
char res3[5] = {};
memop<operator*>(b1, b2, res3, 5);
assert(res3[0] == 0);
assert(res3[1] == 1);
assert(res3[2] == 4);
assert(res3[3] == 9);
assert(res3[4] == 16);
}
困难的是,我不愿意接受由此产生的代码的任何放缓。这意味着暗示间接调用(通过vtable或函数指针)的解决方案是不可行的
这个问题的常见C++解决方案似乎是将运算符包装为在函子类的operator()方法内部调用。通常要获得类似以下代码的东西:
#include <assert.h>
template <typename Op>
void memop(char * buffer1, char * buffer2, char * res, unsigned n){
Op o;
for (unsigned x = 0 ; x < n ; x++){
res[x] = o(buffer1[x], buffer2[x]);
}
};
struct Xor
{
char operator()(char a, char b){
return a ^ b;
}
};
struct Plus
{
char operator()(char a, char b){
return a + b;
}
};
struct Mul
{
char operator()(char a, char b){
return a * b;
}
};
int main(int argc, char ** argv){
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
char res1[5] = {};
memop<Xor>(b1, b2, res1, 5);
assert(res1[0] == 0);
assert(res1[1] == 0);
assert(res1[2] == 0);
assert(res1[3] == 0);
assert(res1[4] == 0);
char res2[5] = {};
memop<Plus>(b1, b2, res2, 5);
assert(res2[0] == 0);
assert(res2[1] == 2);
assert(res2[2] == 4);
assert(res2[3] == 6);
assert(res2[4] == 8);
char res3[5] = {};
memop<Mul>(b1, b2, res3, 5);
assert(res3[0] == 0);
assert(res3[1] == 1);
assert(res3[2] == 4);
assert(res3[3] == 9);
assert(res3[4] == 16);
}
这样做会对性能造成影响吗?
您公开的代码对于benchmark来说几乎是无用的。
char cversion() {
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
char res1[5] = {};
memopXor(b1, b2, res1, 5);
return res1[4];
}
char cppversion() {
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
char res1[5] = {};
memop<Xor>(b1, b2, res1, 5);
return res1[4];
}
被编译为这样的LLVM IR:
define signext i8 @cversion()() nounwind uwtable readnone {
ret i8 0
}
define signext i8 @cppversion()() nounwind uwtable readnone {
ret i8 0
}
也就是说,编译器在编译过程中进行整个计算。
因此,我自由地定义了一个新的功能:
void cppmemopXor(char * buffer1,
char * buffer2,
char * res,
unsigned n)
{
memop<Xor>(buffer1, buffer2, res, n);
}
删除CCD_ 2上的CCD_ 1限定符,然后重复体验:
define void @memopXor(char*, char*, char*, unsigned int)(i8* nocapture %buffer1, i8* nocapture %buffer2, i8* nocapture %res, i32 %n) nounwind uwtable {
%1 = icmp eq i32 %n, 0
br i1 %1, label %._crit_edge, label %.lr.ph
.lr.ph: ; preds = %.lr.ph, %0
%indvars.iv = phi i64 [ %indvars.iv.next, %.lr.ph ], [ 0, %0 ]
%2 = getelementptr inbounds i8* %buffer1, i64 %indvars.iv
%3 = load i8* %2, align 1, !tbaa !0
%4 = getelementptr inbounds i8* %buffer2, i64 %indvars.iv
%5 = load i8* %4, align 1, !tbaa !0
%6 = xor i8 %5, %3
%7 = getelementptr inbounds i8* %res, i64 %indvars.iv
store i8 %6, i8* %7, align 1, !tbaa !0
%indvars.iv.next = add i64 %indvars.iv, 1
%lftr.wideiv = trunc i64 %indvars.iv.next to i32
%exitcond = icmp eq i32 %lftr.wideiv, %n
br i1 %exitcond, label %._crit_edge, label %.lr.ph
._crit_edge: ; preds = %.lr.ph, %0
ret void
}
和带有模板的C++版本:
define void @cppmemopXor(char*, char*, char*, unsigned int)(i8* nocapture %buffer1, i8* nocapture %buffer2, i8* nocapture %res, i32 %n) nounwind uwtable {
%1 = icmp eq i32 %n, 0
br i1 %1, label %_ZL5memopI3XorEvPcS1_S1_j.exit, label %.lr.ph.i
.lr.ph.i: ; preds = %.lr.ph.i, %0
%indvars.iv.i = phi i64 [ %indvars.iv.next.i, %.lr.ph.i ], [ 0, %0 ]
%2 = getelementptr inbounds i8* %buffer1, i64 %indvars.iv.i
%3 = load i8* %2, align 1, !tbaa !0
%4 = getelementptr inbounds i8* %buffer2, i64 %indvars.iv.i
%5 = load i8* %4, align 1, !tbaa !0
%6 = xor i8 %5, %3
%7 = getelementptr inbounds i8* %res, i64 %indvars.iv.i
store i8 %6, i8* %7, align 1, !tbaa !0
%indvars.iv.next.i = add i64 %indvars.iv.i, 1
%lftr.wideiv = trunc i64 %indvars.iv.next.i to i32
%exitcond = icmp eq i32 %lftr.wideiv, %n
br i1 %exitcond, label %_ZL5memopI3XorEvPcS1_S1_j.exit, label %.lr.ph.i
_ZL5memopI3XorEvPcS1_S1_j.exit: ; preds = %.lr.ph.i, %0
ret void
}
正如预期的那样,它们在结构上是相同的,因为函子代码已经完全内联(即使在不了解IR的情况下也是可见的)。
请注意,这不是孤立的结果。例如,std::sort
的执行速度是qsort
的两到三倍,因为它使用了函子而不是间接函数调用。当然,使用模板化函数和函子意味着每个不同的实例化都会生成新的代码,就像您手动编码函数一样,但这正是您手动执行的操作。
只有你才能判断某个东西是否足够快以满足你的需求。但我继续在我自己的盒子上运行你的代码,看看会发生什么。
int main(int argc, char ** argv){
char b1[5] = {0, 1, 2, 3, 4};
char b2[5] = {0, 1, 2, 3, 4};
int ans = 0;
for (int i = 0; i < 100000000; i++) {
char res1[5] = {};
memopXor(b1, b2, res1, 5);
// memop<Xor>(b1, b2, res1, 5);
char res2[5] = {};
memopPlus(b1, b2, res2, 5);
// memop<Plus>(b1, b2, res2, 5);
char res3[5] = {};
memopMul(b1, b2, res3, 5);
// memop<Mul>(b1, b2, res3, 5);
ans += res1[0] + res2[1] + res3[2]; // prevents optimization
}
std::cout << ans << std::endl;
return 0;
}
我在g++上用-O3
编译了这两个版本。time
为手动编码版本返回2.40s,为模板版本返回2.58s。
(顺便说一句,我必须纠正你的memopMul()
才能真正执行乘法。)
我在上面的代码中看到的唯一问题是,编译器在调用memop时会遇到混淆内存操作的问题,请参阅:C++混淆规则。
还要记住,在模板版本中,编译器将为传入的每个唯一模板参数生成一个不同的对象,这意味着对于三个对memop的调用,通过三个不同的操作,您将在二进制文件中获得三个实现。这应该会产生与原始代码几乎相同的代码。
我同意其他评论者的观点,即函数应该内联。不过,如果性能很关键,那么应该在建议的更改前后对代码进行基准测试,为了安全起见,只需要错误的编译器标志就可以把事情搞砸。
- 如何反转整数参数包
- 使用C++库在Android项目中修改gradle中的cmake参数,用于插入指令的测试
- 如何使用默认参数等选择模板专业化
- 模板参数替换失败,并且未完成隐式转换
- 具有默认模板参数的多态类的模板推导失败
- lambda参数转换为constexpr技巧,然后获取带链接的数组
- 将数组作为参数传递给函数安全吗?作为第三方职能部门,可以探索他们想要的之外的其他元素
- 函数调用中参数的顺序重要吗
- 部分定义/别名模板模板参数
- 模板-模板参数推导:三个不同的编译器三种不同的行为
- 使用不带参数的函数访问结构元素
- 基于另一个成员参数将函数调用从类传递给它的一个成员
- 如何在OMNET++中指定与命令行参数组合的输出文件名
- 如何使用Luacneneneba API正确读取字符串和表参数
- 在派生函数中指定void*参数
- 视图中的参数推导失败:take_while
- static_assert在宏中,但也可以扩展到可以用作函数参数的东西
- 使用指向成员的指针将成员函数作为参数传递
- 没有名称的C++模板参数
- 如何将enable-if与模板参数和参数包一起使用