C语言 编译器应该做哪些优化?

9q78igpj  于 2023-03-01  发布在  其他
关注(0)|答案(4)|浏览(149)

假设您已经选择了最有效的算法来解决性能是第一优先级的问题,现在您正在实现它,您必须确定如下细节:
v[i*3+0]v[i*3+1]v[i*3+2]包含粒子i的速度分量,我们要计算总动能,假定所有粒子质量相同,可以写成:

inline double sqr(double x)
{
    return x*x;
}

double get_kinetic_energy(double v[], int n)
{
    double sum = 0.0;

    for (int i=0; i < n; i++)
        sum += sqr(v[i*3+0]) + sqr(v[i*3+1]) + sqr(v[i*3+2]);

    return 0.5 * mass * sum;
}

为了减少乘法的次数,它可以写成:

double get_kinetic_energy(double v[], int n)
{
    double sum = 0.0;

    for (int i=0; i < n; i++)
    {
        double *w = v + i*3;
        sum += sqr(w[0]) + sqr(w[1]) + sqr(w[2]);
    }

    return 0.5 * mass * sum;
}

(one可以用更少的乘法写出一个函数,但这不是这个问题的重点)
现在我的问题是:由于许多C编译器可以自动进行这种优化,开发人员应该在哪些方面依赖编译器,又应该在哪些方面尝试手动进行优化?

mlnl4t2r

mlnl4t2r1#

开发人员应该在哪些方面依赖编译器,以及她/他应该在哪些方面尝试手动进行优化?
1.我对目标硬件以及C代码如何翻译成汇编语言有相当深入的了解吗?如果没有,那就忘了手动优化吧。
1.这段代码中有没有明显的瓶颈--我怎么知道它需要首先进行优化?明显的问题是I/O、复杂循环、忙等待循环、幼稚的算法等等。
1.当我发现这个瓶颈时,我是如何准确地对它进行基准测试的?我确定问题不在于基准测试方法本身吗?来自SO的经验表明,10个奇怪的性能问题中有9个可以用不正确的基准测试来解释。包括:禁用编译器优化的基准测试...
从那时起,您可以开始考虑特定于系统的问题以及算法本身--要考虑的问题太多,无法用一个SO答案来涵盖。这是低端微控制器和64位台式PC(以及两者之间的所有问题)的代码优化之间的巨大差异。

twh00eeo

twh00eeo2#

看看gccclang是如何处理你的代码的,你所考虑的微观优化是徒劳的,编译器已经应用了标准的公共子表达式消除技术,这些技术消除了你试图消除的开销。
实际上,生成的代码使用XMM寄存器一次处理2个组件。
如果性能是必须的,那么下面的步骤将保存这一天:

  • 真实的的评判者是墙上的时钟。2用真实的数据写一个基准测试,然后测量性能,直到你得到一致的结果。
  • 如果你有一个分析器,用它来确定瓶颈在哪里,如果有的话。2改变那些看起来会影响性能的部分的算法是一个有效的方法。
  • 尝试从编译器中获得最佳效果:研究优化选项,尝试让编译器使用更激进的技术(如果它们适合目标系统)。例如,-mavx512f -mavx512cdgcc使用512位ZMM寄存器生成一次处理8个组件的代码。

这是一种非侵入性技术,因为源代码不会更改,所以您不必冒手动优化代码引入新错误的风险。
优化是一门困难的艺术。根据我的经验,简化代码比以可读性和正确性为代价添加额外的微妙内容来提高性能更能获得更好的结果和更少的错误。
查看代码,一个明显的简化似乎会生成相同的结果,并且可能会简化优化器的工作(但同样,让挂钟来判断):

double get_kinetic_energy(const double v[], int n, double mass)
{
    double sum = 0.0;

    for (int i = 0; i < 3 * n; i++)
        sum += v[i] * v[i];

    return 0.5 * mass * sum;
}
vfh0ocws

vfh0ocws3#

有一件事看起来有点像过早的优化,但可能只是对语言能力的无知,那就是你已经拥有了所有的信息来描述扁平化到double值数组中的粒子。
我建议你把它分解开来,通过创建一个结构体来保存每个粒子上的三个数据点,使代码更容易阅读,这样你就可以创建函数,接受单个粒子或多个粒子,并对它们进行计算。
这比向函数传递三倍数量的粒子参数或试图“切片”数组要容易得多。如果这对你来说更容易推理,你就不太可能产生警告/错误。

bprjcwpo

bprjcwpo4#

像clang和gcc这样的编译器同时比许多人所认为的更有能力和更有能力。
它们有非常广泛的模式,可以将代码转换为其他形式,这种形式可能更高效,并且仍然按要求运行。
然而,两者都不擅长判断优化何时有用,都倾向于做出一些近乎滑稽可笑的"优化"决定。
例如,给定

void test(char *p)
{
    char *e = p+5;
    do
    {
        p[0] = p[1];
        p++;
    }while(p < e);
}

针对优化级别低于-O2的Cortex-M0时,gcc 10.2.1将生成等同于调用memmove(p, p+1, 7);的代码。尽管理论上memmove的库实现可能会优化n == 7的情况,从而优于-Og生成的基于字节的五指令循环(甚至是-O0),似乎更有可能的是,任何看似合理的实现都要花一些时间分析需要做什么,然后在分析完之后,花与使用-O0生成的代码一样长的时间执行循环。
实际上,gcc分析循环,找出它要做的事情,然后使用自己的方法以一种可能比程序员一开始想做的更好也可能不好的方式执行该操作。

相关问题