可以使用 char[] 作为参数、返回等来解决任何性能问题

Can using char[] as parameters, return, and etc do any performance issues?

本文关键字:返回 解决 任何 问题 性能 参数 char 可以使      更新时间:2023-10-16

第一个使C++代码更具可读性;我正在编程编译器,我给了它:

var swap = ( int x, y ) => { //Assign method that returns two ints, and gets two ints as parameter to variable named swap.
    var NewX = y
    var NewY = x
}
var increment = ( int x ) => {
    var Result = x + 1
}

注意:函数返回其第一个字母大写的任何变量。 swap可以像... = swap( x, y ).NewX一样使用,但increment可以用作... = increment( x )

经过一些优化后,它生成:(swapincrement实际函数而不是变量,并优化了swap堆栈)

template<int BytesCount> struct rawdata { //struct from some header
    char _[ BytesCount ];
    inline char &operator[] (int index) {
        return _[ index ];
    }
};
//...
rawdata<8> generatedfunction0( rawdata<8> p ) { // var swap = ( int x, y ) => {
    return{ p[ 4 ], p[ 5 ], p[ 6 ], p[ 7 ], p[ 0 ], p[ 1 ], p[ 2 ], p[ 3 ] };
}
rawdata<4> generatedfunction1( rawdata<4> p ) { // var increment = ( int x ) => {
    rawdata<4> r = { p[ 0 ], p[ 1 ], p[ 2 ], p[ 3 ] };
    ++*( ( int* )&r[ 0 ] );
    return r;
}

我几乎可以肯定++*( ( int* )&r[ 0 ] );不会做无用的间接,但是return{ p[ 4 ], p[ 5 ], p[ 6 ], p[ 7 ], p[ 0 ], p[ 1 ], p[ 2 ], p[ 3 ] };呢?是否有任何来源可以保证它会优化它,就好像它是两个整数放入数组中,而不是 8 个或更多个逐字节放置的指令?我不是在谈论这个特殊情况,而是类似的东西。

如果取决于,那么我正在使用 GCC 来编译生成的代码。

是的,它可能会损害性能 - 但并非总是如此。问题在于对单个字节的显式访问。

"智能"编译器会识别出您访问连续内存并尝试对其进行优化。但是由于某种原因,这根本不适用于gcc,clang或icc(尚未测试msvc)。编译器优化器仍有改进的空间,IIRC标准没有要求任何优化。

掉期

因此,让我们处理每个函数,从交换开始。为了完整起见,我又添加了 2 个函数,请参阅代码片段后面:

#include <stdint.h>
rawdata<8> genSWAP(rawdata<8> p)
{
    return { p[ 4 ], p[ 5 ], p[ 6 ], p[ 7 ], p[ 0 ], p[ 1 ], p[ 2 ], p[ 3 ] };
}
rawdata<8> genSWAPvar(rawdata<8> p)
{
    return { p._[ 4 ], p._[ 5 ], p._[ 6 ], p._[ 7 ], p._[ 0 ], p._[ 1 ], p._[ 2 ], p._[ 3 ] };
}
rawdata<8> genSWAP32(rawdata<8> p)
{
    rawdata<8> res = p;
    uint32_t* a = (uint32_t*)&res[0];
    uint32_t* b = (uint32_t*)&res[4];
    uint32_t tmp = *a;
    *a = *b;
    *b = tmp;
    return res;
}
  • genSWAP:您的函数
  • genSWAPvar :与您相同,但不使用您定义的operator[]
  • genSWAP32:明确打包您的字节 32 位每 32 位

您可以在此处查看生成的 asm。

genSWAPgenSWAPvar 没有什么不同,这意味着重载operator[]只是被优化出来了。但是,每个字节在内存中单独访问,也单独处理。这很糟糕,因为在 32 位架构上,处理器一次从内存加载 4 个字节(64 位架构为 8)。因此,简而言之,gcc/clang/icc 正在发出指令来对抗 32 位架构的真正可能性......

genSWAP32效率更高,执行最少数量的加载(对于 32 位),并正确使用寄存器(请注意,对于 64 位架构,应该只能执行一次加载而不是 2 次)。

最后,一些真正的措施:在 Ideone 上,genSWAP32 的速度几乎快了 4 倍(这是有道理的,因为它有 2 个负载而不是 8 个和更少的计算指令)。

增量

同样在这里,您的函数与"优化"函数:

rawdata<4> genINC(rawdata<4> p)
{
    rawdata<4> r = { p[ 0 ], p[ 1 ], p[ 2 ], p[ 3 ] };
    ++*( ( int* )&r[ 0 ] );
    return r;
}
rawdata<4> genINC32(rawdata<4> p)
{
    rawdata<4> res = p;
    uint32_t* a = (uint32_t*)&res[0];
    ++*a;
    return res;
}

生成的 asm 在这里。

对于 clang 和 icc,杀手锏不是增量,而是初始化,您可以在其中单独访问每个字节。 GCC 和 ICC 可能默认这样做,因为字节的顺序可能与0 1 2 3不同。令人惊讶的是,clang 认识到字节的顺序并正确优化了这一点 - 没有性能差异。

然后发生了一些有趣的事情:genINC32函数在 gcc 上较慢,但在 msvc 上更快(*我在rise4fun上没有看到永久链接按钮,所以去那里粘贴在 ideone 上测试的代码)。如果没有看到 msvc 生成的汇编程序并进行比较,我对此没有任何解释。

总之,虽然可以让编译器正确优化所有代码,但现在不要依赖它,所以如果不需要,不要单独访问每个字节。