【问题标题】:Optimization Tips优化提示
【发布时间】:2023-03-22 09:55:01
【问题描述】:
int *s;
allocate memory for s[100];
void func (int *a, int *b)
{
    int i;

    for (i = 0; i < 100; i++)
    {
        s[i] = a[i] ^ b[i];
    }
}

假设这个特定的代码 sn-p 被调用了 1000 次,这是我的代码中最耗时的操作。还假设a和b的地址每次都改变。 's' 是一个全局变量,它使用不同的 a & b 值集进行更新。

据我推测,主要的性能瓶颈是内存访问,因为唯一的其他操作是 XOR,这是非常微不足道的。

您能否建议我如何以最佳方式优化我的代码?

我真正想问的问题,但我认为它没有得到正确传达,例如,这个 for 循环包含 10 个这样的 XOR 操作,循环计数为 100,函数被调用 1000 次,重点是高内存访问..如果代码要在单核机器上执行,有哪些改进空间?

【问题讨论】:

  • 您确定瓶颈不是由于分配内存造成的吗? malloc 涉及定期调用操作系统中昂贵的内存分配代码。例如:在 unix 中调用是 brk()。
  • 这个函数需要线程安全吗?
  • 什么是s?为什么是本地的?如果函数只是填满一个本地数组,它实际上并没有做任何事情,所以优化它的最佳方法是完全删除它。为什么你的函数int 没有返回任何东西?你用这个干什么?您需要向我们提供更多信息。
  • 这个函数很奇怪。它计算s,它被立即丢弃并且分配的内存泄漏。因此,最好的优化可能是int func(int *a, int *b) { (void)a; (void)b; }
  • 您必须提供有关调用函数的更多信息。某些 a 和 b 数组是否重复?当涉及到内存瓶颈时,您确实需要考虑整个访问模式,而不仅仅是 100 个元素的小循环。在稍高的级别重新排序内存访问可能会对性能产生巨大影响,具体取决于发生的情况。

标签: c optimization


【解决方案1】:

我已经测试了建议的解决方案和其他两个。我无法测试 onemasse 的提议,因为保存到 s[] 的结果不正确。我也无法修复它。我不得不对月影代码进行一些更改。测量单位是时钟周期,所以越低越好。

原始代码:

#define MAX 100
void inline STACKO ( struct timespec *ts, struct timespec *te ){

    int i, *s, *a, *b;

    for (i = 0; i < MAX; ++i){
        s = (int *) malloc (sizeof (int)); ++s;
        a = (int *) malloc (sizeof (int)); ++a;
        b = (int *) malloc (sizeof (int)); ++b;
    }

    srand ( 1024 );
    for (i = 0; i < MAX; ++i){
        a[i] = ( rand() % 2 );
        b[i] = ( rand() % 2 );
    }

    rdtscb_getticks ( ts ); /* start measurement */

    for (i = 0; i < MAX; i++)
        s[i] = a[i] ^ b[i];

    rdtscb_getticks ( te ); /* end measurement */

/*
    printf("\n");
    for (i = 0; i < MAX; ++i)
        printf("%d", s[i]);
    printf("\n");
*/
}

新提案1:注册int

发件人:

int i, *s, *a, *b;

收件人:

register int i, *s, *a, *b;

新提案 2:无数组表示法

s_end = &s[MAX];
for (s_ptr = &s[0], a_ptr = &a[0], b_ptr = &b[0]; \
   s_ptr < s_end; \
   ++s_ptr, ++a_ptr, ++b_ptr){
    *s_ptr = *a_ptr ^ *b_ptr;
}

moonshadow 建议优化:

s_ptr = &s[0];
a_ptr = &a[0];
b_ptr = &b[0];

for (i = 0; i < (MAX/4); i++){

    s_ptr[0] = a_ptr[0] ^ b_ptr[0];
    s_ptr[1] = a_ptr[1] ^ b_ptr[1];
    s_ptr[2] = a_ptr[2] ^ b_ptr[2];
    s_ptr[3] = a_ptr[3] ^ b_ptr[3];
    s_ptr+=4; a_ptr+=4; b_ptr+=4;
}

moonshadow 建议优化 + 寄存器 int:

发件人:

int i, *s, ...

收件人:

register int i, *s, ...

Christoffer 提出的优化建议:

#pragma omp for
for (i = 0; i < MAX; i++)
{
    s[i] = a[i] ^ b[i];
}

结果:

Original Code   1036.727264
New Proposal 1  611.147928
New proposal 2  450.788845
moonshadow      713.3845
moonshadow2     452.481192
Christoffer     1054.321943

还有其他简单的方法可以优化生成的二进制文件。将 -O2 传递给 gcc 表示您需要优化。要确切了解 -O2 的作用,请参阅 gcc 手册页。

启用 -O2 后:

Original Code   464.233031
New Proposal 1  452.620255
New proposal 2  454.519383
moonshadow      428.651083
moonshadow2     419.317444
Christoffer     452.079057

源代码在:http://goo.gl/ud52m

【讨论】:

    【解决方案2】:

    不要使用循环变量来索引。 展开循环。

    for (i = 0; i < (100/4); i++)
    {
      s[0] = a[0] ^ b[0];
      s[1] = a[1] ^ b[1];
      s[2] = a[2] ^ b[2];
      s[3] = a[3] ^ b[3];
      s+=4; a+=4; b+=4;
    }
    

    了解如何在您的平台上执行 SIMD XOR。

    将这些 XOR 作为显式步骤执行可能比将它们作为另一个计算的一部分执行更昂贵:您必须从 a 和 b 中读取并将结果存储在 s 中 - 如果再次读取 s 以进行更多计算,您'将通过在此处执行 XOR 来节省每次迭代的读取和写入以及所有函数调用和循环开销;同样,如果 a 和 b 是某些其他函数的输出,则最好在其中一个函数的末尾执行 XOR。

    【讨论】:

    • 你认为编译器不能自己做,给定上限?!它会并且插入适当的 SIMD 指令(如果它们碰巧在这里有用,可能就是这种情况)。
    • @Konrad:我整天都在查看反汇编的编译器输出。很多时候,在我们支持的所有三个平台上,编译器都不能很好地完成这种事情,或者根本就没有。我从未见过编译器在没有显式提示的情况下生成合理的 SIMD 代码(即使用平台的 SIMD 类型和内在函数)。
    • @Konrad:哈哈。理论上,我们的一个平台甚至支持 GCC 自动矢量化 (gcc.gnu.org/projects/tree-ssa/vectorization.html)。在实践中,我现在每个月左右只提交一次针对编译器的错误报告,所以我想情况会有所改善。 Octopiler 发生了什么?
    • 自动矢量化并不是那么容易。编译器需要能够证明 a、b 和 s 可以被 16 整除,如果不能被插入适当的填充,这会增加开销。这种开销实际上可能会恶化代码的性能,当 -fno-tree-vectorize 在某些关键循环上提高性能时,我注意到了这一点。
    • @Konrad:-O3 是不够的。例如,GCC 不会展开循环,除非您明确使用 -funroll-loops,并且不会尝试使用 SIMD 指令,除非您使用 -march=$SOMETHING 指定代码将在支持它们的处理器上运行。
    【解决方案3】:
    int *s;
    allocate memory for s[100];
    void func (int *a, int *b)
    {
        int i;
    
        #pragma omp for
        for (i = 0; i < 100; i++)
        {
            s[i] = a[i] ^ b[i];
        }
    }
    

    当然,只有一百个元素你可能看不到任何特别的改进:-)

    【讨论】:

    • 即使插入您忘记的“并行”,这实际上会对于仅仅 100 个数字。
    • 啊,你说得对,康德拉德。我会删除我之前的评论。
    • 它可能适用于调用此函数 1000 次的下一个更高级别。
    【解决方案4】:

    这里只是猜测。如果这是一个缓存问题,你可以试试这个:

    int *s;
    allocate memory for s[100];
    void func (int *a, int *b)
    {
        int i;
    
        memcpy( s, a, 100 );
    
        for (i = 0; i < 100; i++)
        {
            s[i] = s[i] ^ b[i];
        }
    }
    

    memcpy,虽然它是一个函数调用,但如果 size 参数是一个常量,编译器通常会内联。循环展开在这里可能无济于事,因为它可以由编译器自动完成。但你不应该相信我的话,在你的平台上验证。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-14
      • 2015-11-20
      • 2015-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多