整数与双算术性能?

时间:2010-09-09 12:37:32

标签: c# performance integer double

我正在编写一个C#类,使用整数执行2D可分卷积,以获得比双版本更好的性能。问题是我没有获得真正的性能提升。

这是X过滤器代码(对于int和double两种情况都有效):

foreach (pixel)
{
      int value = 0;
      for (int k = 0; k < filterOffsetsX.Length; k++)
      {
          value += InputImage[index + filterOffsetsX[k]] * filterValuesX[k];  //index is relative to current pixel position
      }
      tempImage[index] = value;
 }

在整数个案“value”中,“InputImage”和“tempImage”属于“int”,“Image <byte>”和“Image <int>”类型。
在双重“值”中,“InputImage”和“tempImage”是“double”,“Image <double>”和“Image <double>”类型。
(filterValues在每种情况下都是int [])
(类Image <T>是extern dll的一部分。它应该类似于.NET Drawing Image类。)。

我的目标是通过int + =(byte * int)vs double + =(double * int)来实现快速性能

以下时间是200次重复的意思 过滤器尺寸9 = 0.031(双)0.027(int)
过滤器尺寸13 = 0.042(双)0.038(int)
过滤器大小25 = 0.078(双倍)0.070(int)

性能提升很小。这可能是由管道停滞和次优代码引起的吗?

编辑:简化删除不重要变量的代码。

EDIT2:我认为我没有与缓存未命中相关的问题,因为“index”遍历相邻的存储单元(逐行方式)。此外,“filterOffstetsX”仅包含相对于同一行上的像素的相对较小的偏移量以及滤波器大小/ 2的最大距离。问题可能出现在第二个可分离滤波器(Y滤波器)中,但时间并没有那么不同。

7 个答案:

答案 0 :(得分:15)

好像你说你只是在你最长的情况下运行那个内循环5000次。最后我检查的FPU(不久前很久以前)只花了大约5个周期来执行乘法而不是整数单位。因此,通过使用整数,您可以节省大约25,000个CPU周期。假设没有缓存未命中或任何其他会导致CPU在任何一种情况下等待的情况。

假设现代英特尔酷睿CPU的时钟频率为2.5Ghz,那么通过使用整数单位,您可以节省大约10 微秒运行时间。有点微不足道。我以实时编程为生,即使我们错过了某个地方的截止日期,我们也不会在这里冒出那么多的CPU浪费。

然而, digEmAll在评论中提出了一个非常好的观点。如果编译器和优化器正在完成他们的工作,整个过程就是流水线操作。这意味着实际上整个内部循环使用FPU运行的时间比整数单元长5个周期,而不是每个操作。如果是这种情况,您的预期时间节省将是如此之小,以至于难以衡量它们。

如果你真的做了足够的浮动操作来让整个shebang需要很长时间,我建议你考虑做以下一项或多项工作:

  1. 并行化算法并在处理器可用的每个CPU上运行它。
  2. 不要在CLR上运行它(使用原生C ++,或Ada或Fortran或其他东西)。
  3. 重写它以在GPU上运行。 GPU本质上是阵列处理器,旨在对浮点值数组进行大规模并行数学运算。

答案 1 :(得分:15)

使用Visual C ++,因为我可以确定我是计时算术运算而不是其他。

结果(每项操作执行6亿次):

i16 add: 834575
i32 add: 840381
i64 add: 1691091
f32 add: 987181
f64 add: 979725
i16 mult: 850516
i32 mult: 858988
i64 mult: 6526342
f32 mult: 1085199
f64 mult: 1072950
i16 divide: 3505916
i32 divide: 3123804
i64 divide: 10714697
f32 divide: 8309924
f64 divide: 8266111

freq = 1562587

CPU是Intel Core i7,Turbo Boosted到2.53 GHz。

基准代码:

#include <stdio.h>
#include <windows.h>

template<void (*unit)(void)>
void profile( const char* label )
{
    static __int64 cumtime;
    LARGE_INTEGER before, after;
    ::QueryPerformanceCounter(&before);
    (*unit)();
    ::QueryPerformanceCounter(&after);
    after.QuadPart -= before.QuadPart;
    printf("%s: %I64i\n", label, cumtime += after.QuadPart);
}

const unsigned repcount = 10000000;

template<typename T>
void add(volatile T& var, T val) { var += val; }

template<typename T>
void mult(volatile T& var, T val) { var *= val; }

template<typename T>
void divide(volatile T& var, T val) { var /= val; }

template<typename T, void (*fn)(volatile T& var, T val)>
void integer_op( void )
{
    unsigned reps = repcount;
    do {
        volatile T var = 2000;
        fn(var,5);
        fn(var,6);
        fn(var,7);
        fn(var,8);
        fn(var,9);
        fn(var,10);
    } while (--reps);
}

template<typename T, void (*fn)(volatile T& var, T val)>
void fp_op( void )
{
    unsigned reps = repcount;
    do {
        volatile T var = (T)2.0;
        fn(var,(T)1.01);
        fn(var,(T)1.02);
        fn(var,(T)1.03);
        fn(var,(T)2.01);
        fn(var,(T)2.02);
        fn(var,(T)2.03);
    } while (--reps);
}

int main( void )
{
    LARGE_INTEGER freq;
    unsigned reps = 10;
    do {
        profile<&integer_op<__int16,add<__int16>>>("i16 add");
        profile<&integer_op<__int32,add<__int32>>>("i32 add");
        profile<&integer_op<__int64,add<__int64>>>("i64 add");
        profile<&fp_op<float,add<float>>>("f32 add");
        profile<&fp_op<double,add<double>>>("f64 add");

        profile<&integer_op<__int16,mult<__int16>>>("i16 mult");
        profile<&integer_op<__int32,mult<__int32>>>("i32 mult");
        profile<&integer_op<__int64,mult<__int64>>>("i64 mult");
        profile<&fp_op<float,mult<float>>>("f32 mult");
        profile<&fp_op<double,mult<double>>>("f64 mult");

        profile<&integer_op<__int16,divide<__int16>>>("i16 divide");
        profile<&integer_op<__int32,divide<__int32>>>("i32 divide");
        profile<&integer_op<__int64,divide<__int64>>>("i64 divide");
        profile<&fp_op<float,divide<float>>>("f32 divide");
        profile<&fp_op<double,divide<double>>>("f64 divide");

        ::QueryPerformanceFrequency(&freq);

        putchar('\n');
    } while (--reps);

    printf("freq = %I64i\n", freq);
}

我使用32位Visual C ++ 2010进行了默认优化构建。

profileaddmultdivide(循环内部)的每次通话均已内联。函数调用仍然生成profile,但由于每次调用都完成了6000万次操作,我认为函数调用开销并不重要。

即使引入了volatile,Visual C ++优化编译器也是 SMART 。我最初使用小整数作为右手操作数,编译器愉快地使用leaadd指令进行整数乘法运算。与普通的智慧所暗示的相比,你可以通过调用高度优化的C ++代码来获得更大的提升,因为C ++优化器比任何JIT都做得好得多。

最初我在循环外部进行了var的初始化,这使得浮点乘法代码因为持续溢出而运行缓慢。 FPU处理NaN很慢,在编写高性能数字运算例程时要记住其他事项。

依赖关系也以防止流水线操作的方式设置。如果你想看到流水线的效果,请在评论中这样说,我将修改测试平台以对多个变量进行操作而不只是一个变量。

i32的反汇编乘法:

;   COMDAT ??$integer_op@H$1??$mult@H@@YAXACHH@Z@@YAXXZ
_TEXT   SEGMENT
_var$66971 = -4                     ; size = 4
??$integer_op@H$1??$mult@H@@YAXACHH@Z@@YAXXZ PROC   ; integer_op<int,&mult<int> >, COMDAT

; 29   : {

  00000 55       push    ebp
  00001 8b ec        mov     ebp, esp
  00003 51       push    ecx

; 30   :    unsigned reps = repcount;

  00004 b8 80 96 98 00   mov     eax, 10000000      ; 00989680H
  00009 b9 d0 07 00 00   mov     ecx, 2000      ; 000007d0H
  0000e 8b ff        npad    2
$LL3@integer_op@5:

; 31   :    do {
; 32   :        volatile T var = 2000;

  00010 89 4d fc     mov     DWORD PTR _var$66971[ebp], ecx

; 33   :        fn(var,751);

  00013 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  00016 69 d2 ef 02 00
    00       imul    edx, 751       ; 000002efH
  0001c 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx

; 34   :        fn(var,6923);

  0001f 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  00022 69 d2 0b 1b 00
    00       imul    edx, 6923      ; 00001b0bH
  00028 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx

; 35   :        fn(var,7124);

  0002b 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  0002e 69 d2 d4 1b 00
    00       imul    edx, 7124      ; 00001bd4H
  00034 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx

; 36   :        fn(var,81);

  00037 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  0003a 6b d2 51     imul    edx, 81            ; 00000051H
  0003d 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx

; 37   :        fn(var,9143);

  00040 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  00043 69 d2 b7 23 00
    00       imul    edx, 9143      ; 000023b7H
  00049 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx

; 38   :        fn(var,101244215);

  0004c 8b 55 fc     mov     edx, DWORD PTR _var$66971[ebp]
  0004f 69 d2 37 dd 08
    06       imul    edx, 101244215     ; 0608dd37H

; 39   :    } while (--reps);

  00055 48       dec     eax
  00056 89 55 fc     mov     DWORD PTR _var$66971[ebp], edx
  00059 75 b5        jne     SHORT $LL3@integer_op@5

; 40   : }

  0005b 8b e5        mov     esp, ebp
  0005d 5d       pop     ebp
  0005e c3       ret     0
??$integer_op@H$1??$mult@H@@YAXACHH@Z@@YAXXZ ENDP   ; integer_op<int,&mult<int> >
; Function compile flags: /Ogtp
_TEXT   ENDS

和f64相乘:

;   COMDAT ??$fp_op@N$1??$mult@N@@YAXACNN@Z@@YAXXZ
_TEXT   SEGMENT
_var$67014 = -8                     ; size = 8
??$fp_op@N$1??$mult@N@@YAXACNN@Z@@YAXXZ PROC        ; fp_op<double,&mult<double> >, COMDAT

; 44   : {

  00000 55       push    ebp
  00001 8b ec        mov     ebp, esp
  00003 83 e4 f8     and     esp, -8            ; fffffff8H

; 45   :    unsigned reps = repcount;

  00006 dd 05 00 00 00
    00       fld     QWORD PTR __real@4000000000000000
  0000c 83 ec 08     sub     esp, 8
  0000f dd 05 00 00 00
    00       fld     QWORD PTR __real@3ff028f5c28f5c29
  00015 b8 80 96 98 00   mov     eax, 10000000      ; 00989680H
  0001a dd 05 00 00 00
    00       fld     QWORD PTR __real@3ff051eb851eb852
  00020 dd 05 00 00 00
    00       fld     QWORD PTR __real@3ff07ae147ae147b
  00026 dd 05 00 00 00
    00       fld     QWORD PTR __real@4000147ae147ae14
  0002c dd 05 00 00 00
    00       fld     QWORD PTR __real@400028f5c28f5c29
  00032 dd 05 00 00 00
    00       fld     QWORD PTR __real@40003d70a3d70a3d
  00038 eb 02        jmp     SHORT $LN3@fp_op@3
$LN22@fp_op@3:

; 46   :    do {
; 47   :        volatile T var = (T)2.0;
; 48   :        fn(var,(T)1.01);
; 49   :        fn(var,(T)1.02);
; 50   :        fn(var,(T)1.03);
; 51   :        fn(var,(T)2.01);
; 52   :        fn(var,(T)2.02);
; 53   :        fn(var,(T)2.03);
; 54   :    } while (--reps);

  0003a d9 ce        fxch    ST(6)
$LN3@fp_op@3:
  0003c 48       dec     eax
  0003d d9 ce        fxch    ST(6)
  0003f dd 14 24     fst     QWORD PTR _var$67014[esp+8]
  00042 dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  00045 d8 ce        fmul    ST(0), ST(6)
  00047 dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  0004a dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  0004d d8 cd        fmul    ST(0), ST(5)
  0004f dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  00052 dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  00055 d8 cc        fmul    ST(0), ST(4)
  00057 dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  0005a dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  0005d d8 cb        fmul    ST(0), ST(3)
  0005f dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  00062 dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  00065 d8 ca        fmul    ST(0), ST(2)
  00067 dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  0006a dd 04 24     fld     QWORD PTR _var$67014[esp+8]
  0006d d8 cf        fmul    ST(0), ST(7)
  0006f dd 1c 24     fstp    QWORD PTR _var$67014[esp+8]
  00072 75 c6        jne     SHORT $LN22@fp_op@3
  00074 dd d8        fstp    ST(0)
  00076 dd dc        fstp    ST(4)
  00078 dd da        fstp    ST(2)
  0007a dd d8        fstp    ST(0)
  0007c dd d8        fstp    ST(0)
  0007e dd d8        fstp    ST(0)
  00080 dd d8        fstp    ST(0)

; 55   : }

  00082 8b e5        mov     esp, ebp
  00084 5d       pop     ebp
  00085 c3       ret     0
??$fp_op@N$1??$mult@N@@YAXACNN@Z@@YAXXZ ENDP        ; fp_op<double,&mult<double> >
; Function compile flags: /Ogtp
_TEXT   ENDS

答案 2 :(得分:4)

您的算法似乎以非连续模式访问大型内存区域。它可能会产生大量的缓存未命中。瓶颈可能是内存访问,而不是算术。使用int应该使这个稍快一些,因为int是32位,而双精度是64位,这意味着缓存将更有效地使用。但是,如果几乎每个循环迭代都涉及缓存未命中,那么除非您可以进行一些算法或数据结构布局更改以改善引用的局部性,否则基本上不运气。

顺便说一下,您考虑过使用FFT进行卷积吗?这会让你进入一个完全不同的大O级。

答案 3 :(得分:2)

至少在32位系统上比较int(DWORD,4字节)和double(QWORD,8字节)是不公平的。将intfloatlongdouble进行比较,以获得公平的结果。 double提高了精确度,你必须付出代价。

PS :对我而言,它闻起来像微(+早熟)优化,而且气味不好。

修改:好的,好的。比较long和double是不正确的,但仍然比较32和CPU上的int和double是不正确的,即使它们同时具有内部指令。这不是魔术,x86是胖的CISC,仍然是双重内部没有单步处理。

答案 4 :(得分:1)

在我的机器上,我发现浮点乘法与整数乘法的速度大致相同。

我正在使用这个计时功能:

static void Time<T>(int count, string desc, Func<T> action){
    action();

    Stopwatch sw = Stopwatch.StartNew();
    for(int i = 0; i < count; i++)
        action();

    double seconds = sw.Elapsed.TotalSeconds;

    Console.WriteLine("{0} took {1} seconds", desc, seconds);
}

假设你正在使用25长度滤波器处理200 x 200阵列200次,那么你的内循环正在执行200 * 200 * 25 * 200 = 200,000,000次。每次,您都要进行一次乘法,一次加法和 3 数组索引。所以我使用这个分析代码

const int count = 200000000;

int[]  a = {1};
double d = 5;
int    i = 5;

Time(count, "array index", ()=>a[0]);
Time(count, "double mult", ()=>d * 6);
Time(count, "double add ", ()=>d + 6);
Time(count, "int    mult", ()=>i * 6);
Time(count, "int    add ", ()=>i + 6);

在我的机器上(我认为比你的慢),我得到以下结果:

array index took 1.4076632 seconds
double mult took 1.2203911 seconds
double add  took 1.2342998 seconds
int    mult took 1.2170384 seconds
int    add  took 1.0945793 seconds

如您所见,整数乘法,浮点乘法和浮点加法都需要大约相同的时间。数组索引需要更长的时间(并且你要做三次),整数加法要快一点。

所以我认为在你的场景中整数数学的性能优势太小而不能产生显着的差异,特别是当你为数组索引支付的相对巨大的惩罚所超过时。如果你真的需要加快速度,那么你应该使用不安全的数组指针来避免偏移计算和边界检查。

顺便说一句,分工的表现差异更为显着。按照上面的模式,我得到:

double div  took 3.8597251 seconds
int    div  took 1.7824505 seconds

还有一点需要注意:

为了清楚起见,所有分析都应该使用优化的发布版本完成。调试版本总体上会较慢,而某些操作可能与其他操作相比没有准确的时序。

答案 5 :(得分:0)

如果您测量的时间是准确的,那么过滤算法的运行时似乎随着过滤器大小的多维数据集而增长。那是什么样的过滤器?也许你可以减少所需的乘法次数。 (例如,如果您使用可分离的过滤器内核?)

否则,如果您需要原始性能,可以考虑使用类似Intel Performance Primitives的库 - 它包含使用CPU SIMD指令的高度优化的功能。它们通常比C#或C ++中的手写代码快得多。

答案 6 :(得分:0)

您是否尝试过查看反汇编代码?在高级语言中,我非常相信编译器可以优化我的代码。 例如,for(i=0;i<imageSize;i++)可能比foreach更快。 此外,算术操作可能会被编译器优化....当你需要优化某些东西时,你要么优化整个“黑盒子”,也许重新发明在那个循环中使用的算法,或者你首先看一下这个问题。代码并看到它的错误