【问题标题】:Is there any performance difference between accessing a hardcode array and a run time initialization array?访问硬编码数组和运行时初始化数组之间是否存在性能差异?
【发布时间】:2015-11-05 21:05:22
【问题描述】:

比如我想用数组SQRT[i]创建一个平方根表来优化一个游戏,但是不知道下面的初始化访问SQRT[i]的值有没有性能差异:

  1. 硬编码数组

    int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255}
    
  2. 在运行时生成值

    int SQRT[65536];
    int main(){
        for(int i=0;i<65536;i++){
            SQRT[i]=sqrt(i);
        }
        //other code
        return 0;
    }
    

访问它们的一些示例:

    if(SQRT[a*a+b*b]>something)
    ...

我不清楚程序是否以不同的方式存储或访问硬代码数组,也不知道编译器是否会优化硬代码数组以加快访问时间,是否有性能访问数组时的区别?

【问题讨论】:

  • 访问时间相同,但数组的初始化/填充将大不相同。
  • 这真的会非常依赖编译器和实现。如果您对特定架构编译器执行的优化感兴趣,请添加此
  • 旁白:考虑将其改为uint8_t 的数组,以使数组的总大小更小。还要考虑避免平方根;通常a*a + b*b &gt; something*something 更好(尽管在这种情况下,这与您编写的测试不同)。
  • 是的,我完全同意@Nawaz。访问时间将相同,但由于第二个代码使用了 sqrt(),它将运行 65536 次,可能会比第一个代码花费更多时间。
  • 您只能靠自己,但如果没有代码生成器,65536 值出现一位数错误的风险是 - 恕我直言,这是不允许的。如果您以后需要更改舍入算法,请仅考虑维护问题...可以考虑 256 或更少的值,但我无法想象输入 65536 值!

标签: c++ c arrays optimization hardcode


【解决方案1】:

首先,你应该做正确的硬编码数组:

static const int SQRT[]={0,1,1,1,2,2,2,2,2,3,3,.......255,255,255};

(同样使用uint8_t而不是int可能会更好,这样可以减小数组的大小,使其对缓存更友好)

这样做比替代方法有一个很大的优势:编译器可以轻松检查数组的内容是否无法更改。

如果没有这个,编译器必须是偏执的——每个函数调用都可能改变SQRT的内容,每个指针都可能指向SQRT,因此任何通过int*char*进行的写入可能正在修改数组。如果编译器无法证明这不会发生,那么这就会限制它可以进行的优化类型,在某些情况下这可能会影响性能。

另一个潜在优势是能够在编译时解决涉及常量的问题。

如果需要,您可以通过巧妙地使用 __restrict__ 来帮助编译器解决问题。

在现代 C++ 中,您可以两全其美;应该可以(并且以合理的方式)编写将在编译时运行的代码,以将SQRT 初始化为constexpr。不过,最好问一个新问题。

【讨论】:

  • 为什么用 uint8_t 而不是 char? char 为 1 个字节。
  • @Bobby: ... 和 char 很可能是 8 位 signed 类型,这意味着它最多有 127。除此之外,使用明确指出您的位宽要求的类型是一个好习惯。
  • @BobbySacamano 另外,由于数组的元素是数字,而不是字符,因此在语义上是不正确的。这与 C++17 引入 std::byte 的原因相同:是时候停止使用 char 处理数字和处理数据字节了。
【解决方案2】:

正如人们在 cmets 中所说:

if(SQRT[a*a+b*b]>something)

是一个可怕的示例用例。如果这就是您需要 SQRT 的全部,只需平方 something

只要您可以告诉编译器SQRT 没有任何别名,那么运行时循环将使您的可执行文件更小,并且只会在启动期间增加少量的 CPU 开销。绝对使用uint8_t,而不是int。从 8 位内存位置加载 32 位临时文件并不比从零填充的 32b 内存位置加载慢。 (x86 上额外的 movsx 指令,而不是使用内存操作数,将在减少缓存污染方面付出更多的代价。RISC 机器通常不允许内存操作数,所以你总是需要一条指令来将值加载到一个寄存器。)

另外,sqrt 在 Sandybridge 上的延迟为 10-21 个周期。如果你不经常需要它,int->double、sqrt、double->int 链并不比 L2 缓存命中差多少。而且比去 L3 或主内存要好。如果你需要很多sqrt,那么当然,制作一个LUT。即使您在桌子上跳来跳去并导致 L1 未命中,吞吐量也会好得多。

您可以通过平方而不是平方来优化初始化,例如

uint8_t sqrt_lookup[65536];
void init_sqrt (void)
{
    int idx = 0;
    for (int i=0 ; i < 256 ; i++) {
        // TODO: check that there isn't an off-by-one here
        int iplus1_sqr = (i+1)*(i+1);
        memset(sqrt_lookup+idx, i, iplus1_sqr-idx);
        idx = iplus1_sqr;
    }
}

您仍然可以获得将sqrt_lookup 设置为const 的好处(编译器知道它不能别名)。要么使用restrict,要么对编译器撒谎,让表的用户看到const 数组,但实际上是你写的。

这可能涉及对编译器撒谎,通过在大多数地方声明extern const,而不是在初始化它的文件中声明。您必须确保这确实有效,并且不会创建引用两个不同符号的代码。如果您只是在初始化它的函数中丢弃const,如果编译器将它放在rodata(或只读bss内存中,如果它未初始化,如果在某些平台上可能的话,您可能会得到一个段错误? )

也许我们可以避免对编译器撒谎,用:

uint8_t restrict private_sqrt_table[65536];  // not sure about this use of restrict, maybe that will do it?
const uint8_t *const sqrt_lookup = private_sqrt_table;

实际上,这只是一个指向const 数据的const 指针,并不能保证它所指向的内容不会被其他引用更改。

【讨论】:

    【解决方案3】:

    访问时间将相同。当您对数组进行硬编码时,在 main 之前调用的 C 库例程将对其进行初始化(在嵌入式系统中,启动代码将读写数据复制,即从 ROM 硬编码到数组所在的 RAM 地址,如果数组是常量,则直接从 ROM 访问)。

    如果使用for循环进行初始化,那么调用Sqrt函数会有开销。

    【讨论】:

    • cpu总是可以直接访问ROM进行数据访问是不正确的。具有 nand flash 的现代系统通常不会使用直接访问!还要记住,有像 AVR 这样使用哈佛架构的系统。在这种情况下,必须在使用前将内容复制到 ram。可能在启动时完成(常见)或在访问期间完成,如使用 gcc 的 AVR 架构。简单来说,此功能与硬件、操作系统和使用的编译器直接相关。
    • 静态/全局数组根本不被代码初始化。文字数据在可执行文件中,映射到内存中。
    猜你喜欢
    • 2015-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-18
    • 2012-03-05
    • 1970-01-01
    • 2020-11-02
    相关资源
    最近更新 更多