实时编程C性能困境

时间:2015-05-10 15:43:31

标签: c performance

我正在开发一个ASM占主导地位的嵌入式架构。我想在C中重构大部分遗留ASM代码,以提高可读性和模块性。

因此,我仍然对细微的细节感到困惑,导致我的希望消失。以下示例中的实际问题要复杂得多,但我想将此作为讨论的切入点。

我的目标是找到最佳的解决方法。

这是原始示例(不要担心代码的作用。我随机编写这个只是为了说明我想谈的问题)。

int foo;
int bar;
int tmp;
int sum;

void do_something() {
    tmp = bar;
    bar = foo + bar;
    foo = foo + tmp;
}

void compute_sum() {
    for(tmp = 1; tmp < 3; tmp++)
        sum += foo * sum + bar * sum;
}

void a_function() {
    compute_sum();
    do_something();
}

使用这个虚拟代码,任何人都会立即删除所有全局变量并用本地变量替换它们:

void do_something(int *a, int *b) {
    int tmp = *b;
    *b = *a + *b;
    *b = tmp + *a;
}

void compute_sum(int *sum, int foo, int bar) {
    int tmp;
    for(tmp = 1; tmp < 3; tmp++)
        sum += *foo * sum + *bar * sum;
}

void a_function(int *sum, int *foo, int *bar) {
    compute_sum(sum, foo, bar);
    do_something(foo, bar);
}

不幸的是,这种返工比原始代码更糟糕,因为所有参数都被推入堆栈,这导致了延迟和更大的代码大小。

everything globals解决方案是最好的解决方案。特别是当源代码大约300k行,具有近3000个全局变量时。

这里我们没有遇到编译器问题,而是一个结构问题。编写漂亮,可移植,可读,模块化和健壮的代码永远不会通过最终的性能测试,因为编译器是愚蠢的,即使是2015年。

另一种解决方案是宁愿选择inline函数。不幸的是,这些函数必须位于头文件中,这也是丑陋的。

编译器无法进一步查看正在处理的文件。当函数标记为extern时,它将不可逆转地导致性能问题。原因是编译器无法对外部声明做出任何假设。

另一方面,链接器可以完成这项工作,并要求编译器通过givin additionalnal信息重建对象文件到编译器。遗憾的是,没有多少编译器提供这样的功能,当它们这样做时,它们会大大减慢构建过程。

我最终遇到了这个困境:

  1. 保持代码丑陋以保持表现

    • 万物全球
    • 无参数的功能(与程序相同)
    • 将所有内容保存在同一个文件中
  2. 遵循标准并编写干净的代码

    • 想想模块
    • 使用定义明确的参数编写小而多的函数
    • 编写小而多的源文件
  3. 目标架构的资源有限时该怎么办。回到集会是我的最后一个选择。

    其他信息

    我正在开发一个非常强大的哈佛CISC架构的SHARC架构。不幸的是,一个代码指令需要48位,而long只需要32位。有了这个事实,最好保持变量的版本,而不是动态评估第二个值:

    优化示例:

    int foo;
    int bar;
    int half_foo;
    
    void example_a() {
       write(foo); 
       write(half_foo + bar);
    }
    

    坏人:

    void example_a(int foo, int bar) {
       write(foo); 
       write(bar + (foo >> 1));
    }
    

3 个答案:

答案 0 :(得分:7)

丑陋的C代码仍然比汇编程序更具可读性。此外,您可能会获得一些意想不到的免费优化。

  

编译器无法进一步查看正在处理的文件。当函数标记为extern时,它将不可逆转地导致性能问题。原因是编译器无法对外部声明做出任何假设。

假和假。你尝试过“整个程序优化”吗?内联函数的好处,无需组织成标题。如果你整理标题,那么把东西放在标题中并不一定很难看。

在VisualDSP ++编译器中,这由-ipa开关启用。

  

ccts编译器具有称为过程间分析(IPA)的功能,a   允许编译器跨翻译单元进行优化的机制   而不是只在一个翻译单元内。这种能力有效   允许编译器查看最终链接中使用的所有源文件   在编译时,并在优化时使用该信息。

     

在初始链接之后调用所有-ipa优化   一个叫做预链接器的特殊程序重新启动编译器来执行   新的优化。

答案 1 :(得分:4)

我曾经在性能关键的核心/内核类型领域工作,需求非常紧张,通常有利于接受优化程序和标准库性能,并且有一些盐(例如:不要太兴奋malloc或自动生成矢量化的速度。

然而,我从来没有过这么紧迫的需求,以便使指令的数量或推动更多参数的速度成为一个相当大的问题。如果它确实是目标系统和性能测试失败的一个主要问题,那么需要注意的一点是,在微观粒度级别建模的性能测试通常会让您着迷于最小的微效率。

微效率性能测试

我们错误地在以前的工作场所编写了各种表面的微观级别测试,我在那里进行测试,只是简单地计算一些基本的东西,比如从文件中读取一个32位浮点数。同时,我们进行了优化,大大加快了与读取和解析整个文件内容相关的广泛,真实世界的测试用例,同时,一些超级微测试实际上因某些未知的原因而变慢(他们甚至没有被直接修改,但是对它们周围的代码的更改可能会对高速缓存,分页等动态因素产生一些间接影响,或仅仅是优化器如何处理这样的代码。)

因此,当您使用高级语言而不是汇编语言时,微观层面的世界会变得更加混乱。青少年事物的表现可能会在你的脚下发生一些变化,但你必须问自己更重要的是:从文件读取一个32位浮点数或者拥有真实世界的性能略有下降从整个文件读取的操作要快得多。在更高级别对性能测试和性能分析会话建模将为您提供选择性和高效优化真正重要的部分的空间。在那里,你有许多方法给猫皮肤。

在一个超级粒度的操作上运行一个探测器,反复执行了一百万次,你本来就已经支持自己进入一个装配类型的微角落,只是根据你的剖析方式进行这种微观级别的测试。代码。所以你真的想在那里缩小一点,在更粗糙的水平上测试一些东西,这样你就可以像一个训练有素的狙击手一样,磨练非常精选的部分的微观效率,派遣领导者效率低下而不是试图成为一个英雄将可能成为行为障碍的每一个微不足道的步兵都拿去。

优化链接器

您的一个误解是只有编译器才能充当优化器。链接目标文件时,链接器可以执行各种优化,包括内联代码。因此,如果有的话,应该很少需要将所有内容都作为优化插入到单个目标文件中。如果您发现其他情况,我会尝试更多地查看链接器的设置。

界面设计

除了这些东西之外,可维护的大规模代码库的关键在于接口(即头文件)而不是实现(源文件)。如果你的汽车发动机每小时行驶一千英里,你可能会在引擎盖下找到并发现几乎没有喷火的恶魔在周围跳舞以允许这种情况发生。也许有一个与魔鬼有关的协议来获得这样的速度。但是你不必向开车的人揭露这个事实。你仍然可以给他们一套很好的直观,安全的控制来驱动那个野兽。

所以你可能有一个系统可以使无内衬的函数调用成本高昂,但相对于什么来说却昂贵?如果你正在调用一个对一百万个元素进行排序的函数,那么无论你正在处理什么样的硬件,将指针和整数等一些小参数推送到堆栈的相对成本应该是绝对微不足道的。在函数内部,你可以做各种各样的探查器辅助的东西,以提高性能,如宏,强制内联代码,无论什么,甚至一些内联汇编,但保持代码在整个系统中级联复杂的关键是保持所有恶魔代码都隐藏在使用您的排序功能的人身上,并确保它经过充分测试,以便人们不必不断地试图找出故障源

忽略相对于什么?&#39;问题,只关注绝对性也是导致微观剖析的原因,这种微观剖析可能会产生反效果,而不是有用的。

所以我建议从公共界面设计层面更多地看一下这个问题,因为在界面背后,如果你看到幕后/幕后,你可能会发现各种各样的邪恶事情在剖析器中显示的热点区域中需要性能优势。但是,如果你的接口经过精心设计和经过充分测试,你就不应该经常使用引擎盖。

全球范围变得越大,问题就越大。如果您在源文件中静态定义了全局内部链接,而其他任何人都无法访问,那么这些实际上是非常本地的#39;全局。如果没有关注线程安全性(如果是,那么你应该尽可能地避免使用可变全局变量),那么你的代码库中可能会有许多性能关键区域,如果你在引擎盖下,那么你查找文件范围 - 静态变量以减轻函数调用的开销。这仍然比装配更容易维护,特别是当这些全局变量的可见性随着越来越小的源文件的减少而变得越来越小,这些源文件专用于执行更明确,更明确的职责。

答案 2 :(得分:0)

我设计/编写/测试/记录了许多实时嵌入式系统。

'软'实时和'硬'实时。

我可以从经验丰富的经验告诉您,用于实现应用程序的算法是获得最大速度提升的地方。

与内联相比,函数调用之类的小东西是微不足道的,除非执行数千次(甚至数十万次)