加速C ++代码

时间:2010-05-11 14:20:21

标签: c++ optimization

我正在编写一个C ++数字运算应用程序,其中瓶颈是一个必须计算为double的函数:

 template<class T> inline T sqr(const T& x){return x*x;}

和另一个计算

的人
Base   dist2(const Point& p) const
       { return sqr(x-p.x) + sqr(y-p.y) + sqr(z-p.z); }

这些操作占用了80%的计算时间。我想知道你是否可以建议使其更快的方法,即使存在某种准确性损失

由于

21 个答案:

答案 0 :(得分:14)

首先,确保dist2可以内联(从你的帖子中不清楚是否是这种情况),如果有必要在头文件中定义它(通常你需要这样做 - 但是如果你的编译器在链接时生成代码,然后不一定是这样。)

假设x86架构,请确保允许编译器使用SSE2指令(SIMD指令集的示例)生成代码(如果它们在目标体系结构上可用)。为了给编译器提供优化这些优化的最佳机会,您可以尝试将sqr操作一起批处理(SSE2指令一次最多可以执行4次浮点运算或2次双操作,具体取决于指令..但当然它可以只有在准备好的多个操作的输入时才执行此操作。我不会过于乐观地认为编译器能够确定它可以批量处理它们。但是你至少可以设置你的代码以便理论上可以。

如果你仍然对速度不满意并且你不相信你的编译器做得最好,你应该考虑使用编译器内在函数,这将允许你明确地编写潜在的并行指令..或者你,你可以直接编写特定于体系结构的汇编代码,以利用SSE2或最适合您架构的指令。 (警告:如果你手工编写程序集,要么特别注意它仍然被内联,或者使它成为一个大批量操作)

为了更进一步,(并且就像glowcoder已经提到的那样)你可以在GPU上执行这些操作。对于您的具体情况,请记住GPU通常不支持双精度浮点数。虽然如果它非常适合您正在做的事情,您将以这种方式获得更好的性能。 Google for GPGPU或诸如此类的东西,看看哪种方式最适合你。

答案 1 :(得分:10)

什么是Base

它是一个非显式构造函数的类吗?您可能正在创建大量临时Base对象。这可能是一个很大的CPU。

template<class T> inline T sqr(const T& x){return x*x;}
Base   dist2(const Point& p) const {
  return sqr(x-p.x) + sqr(y-p.y) + sqr(z-p.z);
}

如果p的成员变量属于Base类型,您可以在基础对象上调用sqr,这将为sqr中的减去坐标创建临时值},然后为每个添加的组件。

(没有类定义我们无法分辨)

你可以通过强制将sqr调用放在原始状态而不是使用Base来加速它,直到你得到返回类型dist2

其他绩效改善机会是:

  • 如果精度较低,请使用非浮点运算。
  • 使用不需要调用dist2的算法,可能是缓存或使用传递属性。
  • (这可能是显而易见的,但是)请确保您在开启优化时进行编译。

答案 2 :(得分:8)

我认为优化这些函数可能很困难,您可能最好优化调用这些函数的代码来减少调用它们,或者以不同方式执行操作。

答案 3 :(得分:8)

您没有说明对dist2的调用是否可以并行化。如果可以的话,那么你可以构建一个线程池,并将每个线程的工作分成更小的块。

您的探查者告诉您在dist2内发生了什么。您是否实际上一直使用100%CPU,或者您是否缺少缓存并等待加载数据?

老实说,我们真的需要更多细节才能给你一个明确的答案。

答案 4 :(得分:6)

如果sqr()仅用于基本类型,则可以尝试按值而不是引用来获取参数。这样可以节省你的间接费用。

答案 5 :(得分:6)

如果您可以适当地组织数据,那么您可以在此处使用SIMD优化。为了有效实现,您可能希望填充Point结构,使其具有4个元素(即添加第四个虚拟元素进行填充)。

答案 6 :(得分:5)

如果您有许多这样做,并且您正在进行图形或“图形”任务(热建模,几乎任何3D建模),您可以考虑使用OpenGL并将任务卸载到GPU。这将允许计算并行运行,具有高度优化的运行能力。毕竟,你会期望像distance或distanceq这样的东西在GPU上拥有自己的操作码。

当地大学的一名研究人员将几乎所有用于人工智能的三维计算工作卸载到GPU并获得了更快的结果。

答案 7 :(得分:4)

有很多答案已经提到了SSE ......但是由于没有人提到如何使用它,我会把另一个提到......

除了两个约束:别名和对齐外,你的代码拥有矢量化器需要工作的大部分内容。

  • 别名是两个名称引用两个相同对象的问题。例如,my_point.dist2( my_point )将在my_point的两个副本上运行。这与矢量化器混淆。

    C99为指针定义了关键字restrict,以指定引用的对象是唯一引用的:在当前作用域中没有指向该对象的其他restrict指针。大多数不错的C ++编译器也实现了C99,并以某种方式导入了这个功能。

    • GCC称之为__restrict__。它可能适用于参考文献或this
    • MSVC将其称为__restrict。如果支持与GCC有任何不同,我会感到惊讶。

    (但它不是在C ++ 0x中。)

    #ifdef __GCC__
    #define restrict __restrict__
    #elif defined _MSC_VER
    #define restrict __restrict
    #endif
     
    Base   dist2(const Point& restrict p) const restrict
    
  • 大多数SIMD单元需要与矢量大小对齐。 C ++和C99保留了对齐实现定义,但C ++ 0x通过引入[[align(16)]]赢得了这场竞赛。由于未来仍有一点,您可能需要编译器的半便携式支持,即restrict

    #ifdef __GCC__
    #define align16 __attribute__((aligned (16)))
    #elif defined _MSC_VER
    #define align16 __declspec(align (16))
    #endif
     
    struct Point {
        double align16 xyz[ 3 ]; // separate x,y,z might work; dunno
        …
    };
    

这不能保证产生结果; GCC和MSVC都会实施有用的反馈,告诉您什么没有矢量化以及为什么。谷歌你的矢量化器了解更多。

答案 8 :(得分:4)

我建议采用两种技巧:

  1. 将结构成员移动到 开头的局部变量。
  2. 一起执行类似的操作。
  3. 这些技术可能没有区别,但值得尝试。 在进行任何更改之前,请先打印汇编语言。这将为您提供比较基准。

    以下是代码:

    Base   dist2(const Point& p) const
    {
        //  Load the cache with data values.
        register x1 = p.x;
        register y1 = p.y;
        register z1 = p.z;
    
        // Perform subtraction together
        x1 = x - x1;
        y1 = y - y1;
        z1 = z - z2;
    
        // Perform multiplication together
        x1 *= x1;
        y1 *= y1;
        z1 *= z1;
    
        // Perform final sum
        x1 += y1;
        x1 += z1;
    
        // Return the final value
        return x1;
    }
    

    另一种选择是按维度分组。例如,首先执行所有“X”操作,然后执行Y,然后执行Z。这可能会向编译器显示这些片段是独立的,并且可以委托给另一个核心或处理器。

    如果你不能从这个功能中获得更多的表现,你应该像其他人建议的那样去寻找其他地方。另请阅读Data Driven Design。有些例子可以重新组织数据加载,使性能提高25%以上。

    此外,您可能希望使用系统中的其他处理器进行调查。例如,BOINC Project可以将计算委托给图形处理器。

    希望这有帮助。

答案 9 :(得分:4)

如果你真的需要所有dist2值,那么你必须计算它们。它已经处于低水平,除了分布在多个核心之外,无法想象加速。

另一方面,如果你正在寻找亲密度,那么你可以向dist2()函数提供当前的miminum值。这样,如果sqr(x-p.x)已经大于您当前的最小值,则可以避免计算剩余的2个方格。

此外,您可以通过更深入的双重表示来避免第一个方格。直接将指数值与当前的miminum进行比较可以节省更多的周期。

答案 10 :(得分:4)

您使用的是Visual Studio吗?如果是这样,您可能希望使用/fp fast作为编译开关来指定浮点单元控件。看看The fp:fast Mode for Floating-Point Semantics。 GCC有一些你可能想要考虑的-fopTION浮​​点优化(如果你说的话,准确性不是一个大问题)。

答案 11 :(得分:3)

从操作计数来看,我没有看到如何在不深入研究其他人指出的硬件优化(如SSE)的情况下加速。另一种方法是使用different norm,就像1范数只是术语绝对值的总和。然后不需要乘法。但是,这会通过重新排列对象的表观间距来更改空间的基础几何图形,但这可能对您的应用程序无关紧要。

答案 12 :(得分:3)

浮点运算经常慢,也许你可以考虑修改代码只使用整数运算,看看这是否有帮助?

编辑:在Paul R提出的观点之后,我重写了我的建议,不要声称浮点运算总是更慢。谢谢。

答案 13 :(得分:2)

你最好的希望是仔细检查每个dist2调用是否真的需要:调用它的算法可以重构为更高效吗?如果多次计算某些距离,可能会缓存它们吗?

如果您确定所有调用都是必要的,那么您可以通过使用体系结构感知编译器来挤出最后一滴性能。例如,我在x86s上使用Intel's compiler得到了很好的结果。

答案 14 :(得分:2)

只是一些想法,但不太可能在18个答案后添加任何有价值的内容:)

如果您在这两个功能上花费80%的时间,我可以想象两种典型的场景:

您的算法至少是多项式
由于您的数据似乎是空间的,您可以通过引入空间索引来降低O(n)吗?

您正在循环某些设置
如果此集来自磁盘上的数据(已排序?)或来自循环,则可能会缓存,或使用以前的计算来更快地计算sqrt。

关于缓存,您应该定义所需的精度(和输入范围) - 也许可以使用某种查找/缓存?

答案 15 :(得分:0)

查看上下文。您无法像x*x那样简单地优化操作。 相反,你应该看一个更高的层次:从哪里调用函数?多常?为什么?你能减少通话次数吗?你能使用SIMD指令一次对多个元素进行乘法运算吗?

您是否可以将算法的整个部分卸载到GPU?

是否定义了函数以便内联? (基本上,它的定义在呼叫站点可见)

计算后是否需要立即得到结果?如果是这样,FP操作的延迟可能会伤害您。尝试安排代码,以便依赖链被分解或与不相关的指令交错。

当然,检查生成的程序集,看看它是否符合您的期望。

答案 16 :(得分:0)

(刮擦!!! sqr!= sqrt)

查看“Fast sqrt”是否适用于您的情况:

http://en.wikipedia.org/wiki/Fast_inverse_square_root

答案 17 :(得分:0)

您是否有理由实施自己的sqr运算符?

你是否尝试过libm中的那个应该高度优化。

答案 18 :(得分:0)

我发生的第一件事是memoization(函数调用的即时缓存),但sqr和dist2看起来它们的级别太低,无法与memoization相关联的开销弥补由于记忆而节省。但是在更高级别,您可能会发现它可能适合您。

我认为需要对您的数据进行更详细的分析。说程序中的大部分时间花在执行MOV和JUMp命令上可能是准确的,但它不会帮助yhou优化很多。信息太低了。例如,如果你知道整数参数对dist2足够好,并且值在0到9之间,那么预先缓存的表将是1000个元素 - 不是很大。您始终可以使用代码生成它。

你是否展开了循环?分解矩阵运算?寻找可以通过表查找而不是实际计算得到的地方。

最激动人心的是采用以下所述的技术: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.115.8660&rep=rep1&type=pdf 虽然这是一个难以理解的读者,如果你不熟悉Common Lisp,你应该得到一些帮助。

答案 19 :(得分:0)

我很好奇为什么当你说使用双打完成计算时你把它作为模板? 为什么不写一个标准的方法,函数,或只是'x * x'?

如果您的输入可以被预测限制并且确实需要速度,则创建一个包含您的函数可以生成的所有输出的数组。使用输入作为数组的索引(稀疏哈希)。然后,函数评估成为比较(测试数组边界),加法和内存引用。它不会比这快得多。

答案 20 :(得分:0)

请参阅SUBPD,MULPD和DPPD说明。 (DPPD要求SSE4)

取决于您的代码,但在某些情况下,数组结构布局可能比矢量化布局更友好。