【问题标题】:C versus vDSP versus NEON - How could NEON be as slow as C?C 与 vDSP 与 NEON - NEON 怎么能像 C 一样慢?
【发布时间】:2013-02-02 07:06:38
【问题描述】:

NEON 怎么可能和 C 一样慢?

我一直在尝试构建一个快速的直方图函数,通过为传入的值分配一个值来将它们存储到范围中——这是它们最接近的范围阈值。这是将应用于图像的东西,所以它必须很快(假设一个 640x480 的图像数组,所以 300,000 个元素)。直方图范围数是 (0,25,50,75,100) 的倍数。输入是浮点数,最终输出显然是整数

我通过打开一个新的空项目(无应用程序委托)并仅使用 main.m 文件在 xCode 上测试了以下版本。我删除了除 Accelerate 之外的所有链接库。

这是 C 实现:旧版本有很多 if then 但这是最​​终优化的逻辑。耗时 11 秒和 300 毫秒。

int main(int argc, char *argv[])
{
  NSLog(@"starting");

  int sizeOfArray=300000;

  float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
  int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray);

  for (int i=0; i<sizeOfArray; ++i)
  {
    inputArray[i]=88.5;
  }

  //Assume range is [0,25,50,75,100]
  int lcd=25;

  for (int j=0; j<1000; ++j)// just to get some good time interval
  {
    for (int i=0; i<sizeOfArray; ++i)
    {
        //a 60.5 would give a 50. An 88.5 would give 100
        outputArray[i]=roundf(inputArray[i]/lcd)*lcd;
    }
  }
NSLog(@"done");
}

这里是 vDSP 实现。即使有一些繁琐的来回浮动到整数,也只用了 6s!几乎提高了 50%!

//vDSP implementation
 int main(int argc, char *argv[])
 {
   NSLog(@"starting");

   int sizeOfArray=300000;

   float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
   float* outputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);//vDSP requires matching of input output
   int* outputArray=(int*) malloc(sizeof(int)*sizeOfArray); //rounded value to the nearest integere
   float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);
   int* finalOutputArray=(int*) malloc(sizeof(int)*sizeOfArray); //to compare apples to apples scenarios output


   for (int i=0; i<sizeOfArray; ++i)
   {
     inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
   }


   for (int j=0; j<1000; ++j)// just to get some good time interval
   {
     //Assume range is [0,25,50,75,100]
     float lcd=25.0f;

     //divide by lcd
     vDSP_vsdiv(inputArray, 1, &lcd, outputArrayF, 1,sizeOfArray);

     //Round to nearest integer
     vDSP_vfixr32(outputArrayF, 1,outputArray, 1, sizeOfArray);

     // MUST convert int to float (cannot just cast) then multiply by scalar - This step has the effect of rounding the number to the nearest lcd.
    vDSP_vflt32(outputArray, 1, outputArrayF, 1, sizeOfArray);
    vDSP_vsmul(outputArrayF, 1, &lcd, finalOutputArrayF, 1, sizeOfArray);
    vDSP_vfix32(finalOutputArrayF, 1, finalOutputArray, 1, sizeOfArray);
   }
  NSLog(@"done");
}

这里是 Neon 的实现。这是我的第一次,所以玩得很好!它比 vDSP 慢,需要 9 秒和 300 毫秒,这对我来说没有意义。要么 vDSP 比 NEON 优化得更好,要么我做错了什么。

//NEON implementation
int main(int argc, char *argv[])
{
NSLog(@"starting");

int sizeOfArray=300000;

float* inputArray=(float*) malloc(sizeof(float)*sizeOfArray);
float* finalOutputArrayF=(float*) malloc(sizeof(float)*sizeOfArray);

for (int i=0; i<sizeOfArray; ++i)
{
    inputArray[i]=37.0; //this will produce an final number of 25. On the other hand 37.5 would produce 50.
}



for (int j=0; j<1000; ++j)// just to get some good time interval
{
    float32x4_t c0,c1,c2,c3;
    float32x4_t e0,e1,e2,e3;
    float32x4_t f0,f1,f2,f3;

    //ranges of histogram buckets
    float32x4_t buckets0=vdupq_n_f32(0);
    float32x4_t buckets1=vdupq_n_f32(25);
    float32x4_t buckets2=vdupq_n_f32(50);
    float32x4_t buckets3=vdupq_n_f32(75);
    float32x4_t buckets4=vdupq_n_f32(100);

    //midpoints of ranges
    float32x4_t thresholds1=vdupq_n_f32(12.5);
    float32x4_t thresholds2=vdupq_n_f32(37.5);
    float32x4_t thresholds3=vdupq_n_f32(62.5);
    float32x4_t thresholds4=vdupq_n_f32(87.5);


    for (int i=0; i<sizeOfArray;i+=16)
    {
        c0= vld1q_f32(&inputArray[i]);//load
        c1= vld1q_f32(&inputArray[i+4]);//load
        c2= vld1q_f32(&inputArray[i+8]);//load
        c3= vld1q_f32(&inputArray[i+12]);//load


        f0=buckets0;
        f1=buckets0;
        f2=buckets0;
        f3=buckets0;

        //register0
        e0=vcgtq_f32(c0,thresholds1);
        f0=vbslq_f32(e0, buckets1, f0);

        e0=vcgtq_f32(c0,thresholds2);
        f0=vbslq_f32(e0, buckets2, f0);

        e0=vcgtq_f32(c0,thresholds3);
        f0=vbslq_f32(e0, buckets3, f0);

        e0=vcgtq_f32(c0,thresholds4);
        f0=vbslq_f32(e0, buckets4, f0);



        //register1
        e1=vcgtq_f32(c1,thresholds1);
        f1=vbslq_f32(e1, buckets1, f1);

        e1=vcgtq_f32(c1,thresholds2);
        f1=vbslq_f32(e1, buckets2, f1);

        e1=vcgtq_f32(c1,thresholds3);
        f1=vbslq_f32(e1, buckets3, f1);

        e1=vcgtq_f32(c1,thresholds4);
        f1=vbslq_f32(e1, buckets4, f1);


        //register2
        e2=vcgtq_f32(c2,thresholds1);
        f2=vbslq_f32(e2, buckets1, f2);

        e2=vcgtq_f32(c2,thresholds2);
        f2=vbslq_f32(e2, buckets2, f2);

        e2=vcgtq_f32(c2,thresholds3);
        f2=vbslq_f32(e2, buckets3, f2);

        e2=vcgtq_f32(c2,thresholds4);
        f2=vbslq_f32(e2, buckets4, f2);


        //register3
        e3=vcgtq_f32(c3,thresholds1);
        f3=vbslq_f32(e3, buckets1, f3);

        e3=vcgtq_f32(c3,thresholds2);
        f3=vbslq_f32(e3, buckets2, f3);

        e3=vcgtq_f32(c3,thresholds3);
        f3=vbslq_f32(e3, buckets3, f3);

        e3=vcgtq_f32(c3,thresholds4);
        f3=vbslq_f32(e3, buckets4, f3);


        vst1q_f32(&finalOutputArrayF[i], f0);
        vst1q_f32(&finalOutputArrayF[i+4], f1);
        vst1q_f32(&finalOutputArrayF[i+8], f2);
        vst1q_f32(&finalOutputArrayF[i+12], f3);
    }
}
NSLog(@"done");
}

PS:这是我第一次进行这种规模的基准测试,所以我尽量保持简单(大循环,设置代码不变,使用 NSlog 打印开始/结束时间,只加速框架链接)。如果这些假设中的任何一个对结果有重大影响,请批评。

谢谢

【问题讨论】:

  • 这里有什么问题?
  • 哎呀,在战壕里迷路了。更新了上面的问题。
  • 这是什么@"..." 东西?这真的是 C 吗?
  • 我的错,我可以用 printf 替换它。我猜既然这三种情况都很常见,那么它不应该是一个因素。
  • @FUZxxl - @"..." 只是 Objective-C 定义字符串文字的方式。所以不,这不是 C。

标签: objective-c assembly arm neon vdsp


【解决方案1】:

作为对 Rob 回答的补充,编写 NEON 本身就是一门艺术(顺便说一句,感谢您插入我的 Wandering Coder 帖子)和 auselen 的回答(您确实有太多的寄存器在任何给定时间,导致溢出),我应该补充一点,你的内在算法比其他两个更通用:它允许任意范围,而不仅仅是倍数,所以你试图比较不可比较的东西。总是将橙子与橙子进行比较;但是,如果您只需要自定义算法的特定功能,那么比较一种比现成通用算法更具体的自定义算法是公平的游戏。所以这是 NEON 算法与 C 算法一样慢的另一种方式:如果它们不是同一个算法。

至于您的直方图需求,暂时使用您使用 vDSP 构建的内容,如果性能对您的应用程序不满意,然后再研究另一种优化方式;这样做的途径,除了使用 NEON 指令外,还包括避免过多的内存移动(可能是 vDSP 实现中的瓶颈),并在浏览像素时增加每个桶的计数器,而不是让这个中间输出由强制价值观。高效的 DSP 代码不仅与计算本身有关,还与如何最有效地使用内存带宽等有关。在移动设备上更是如此:内存 I/O,甚至是缓存,比处理器内核中的操作更耗电,因此两个内存 I/O 总线往往以处理器时钟速度的较低部分运行,所以你没有那么多内存带宽可以使用,并且你应该明智地使用你拥有的内存带宽,因为任何使用它都会消耗能量。

【讨论】:

  • Pierre,非常感谢您的建议,尤其是缓存洞察力。
【解决方案2】:

首先,这不是“NEON”本身。这是内在的。在 clang 或 gcc 下使用内部函数几乎不可能获得良好的 NEON 性能。如果你认为你需要内在函数,你应该手写汇编程序。

vDSP 并不比 NEON“优化得更好”。 iOS 上的 vDSP 使用 NEON 处理器。 vDSP 对 NEON 的使用比您对 NEON 的使用优化得多。

我还没有深入研究您的内在代码,但最有可能(实际上几乎可以肯定)的问题原因是您正在创建等待状态。用汇编程序编写(而内在函数只是戴着焊接手套编写的汇编程序),与用 C 编写完全不同。你不会循环相同。你比较的不一样。你需要一种新的思维方式。在汇编中,您一次可以做不止一件事(因为您有不同的逻辑单元),但是您绝对必须以所有这些事情可以并行运行的方式安排事情。良好的组装使所有这些管道保持满。如果您可以阅读您的代码并且它非常有意义,那么它可能是垃圾汇编代码。如果您从不重复自己,那可能是垃圾汇编代码。您需要仔细考虑进入哪个寄存器的内容以及在您被允许读取之前有多少个周期。

如果它像音译 C 一样简单,那么编译器会为您做到这一点。当你说“我要用 NEON 写这个”时,你就是在说“我认为我可以写出比编译器更好的 NEON”,因为编译器也使用它。也就是说,通常可以编写比编译器更好的 NEON(尤其是 gcc 和 clang)。

如果您准备好进入那个世界(而且这是一个非常酷的世界),那么您可以阅读一些内容。以下是我推荐的一些地方:

说了这么多...总是总是从重新考虑你的算法开始。答案往往不是如何让你的循环快速计算,而是如何不那么频繁地调用循环。

【讨论】:

  • Rob,感谢 cmets 的帮助,尤其是 hilbert 和 SO 问题。所以我想,如果我必须去NEON;我需要去组装。在我这样做之前,我应该在 vDSP 方面尝试更多的调整。 PS:非常喜欢你的书 iOS6 PTL。
  • @Spectravideo - 答案的最后一句话非常重要。优化的一条经验法则是 - “做某事的最快方法是根本不做。”。算法是必不可少的。
【解决方案3】:

ARM NEON 有 32 个寄存器,64 位宽(双视图为 16 个寄存器,128 位宽)。您的 neon 实现已经使用了至少 18 128 位宽,因此编译器会生成代码以在堆栈中来回移动它们,这并不好 - 太多额外的内存访问。

如果您打算使用汇编,我发现最好使用工具将指令转储到目标文件中。一个在 Linux 中称为 objdump,我相信在 Apple 世界中称为 otool。通过这种方式,您实际上可以看到生成的机器代码是什么样的,以及编译器对您的函数做了什么。

以下是您的 neon 实现从 gcc (-O3) 4.7.1 转储的部分内容。您可以注意到通过vldmia sp, {d8-d9} 加载了一个四元寄存器。

1a6:    ff24 cee8   vcgt.f32    q6, q10, q12
1aa:    ff64 4ec8   vcgt.f32    q10, q10, q4
1ae:    ff2e a1dc   vbit    q5, q15, q6
1b2:    ff22 ceea   vcgt.f32    q6, q9, q13
1b6:    ff5c 41da   vbsl    q10, q14, q5
1ba:    ff20 aeea   vcgt.f32    q5, q8, q13
1be:    f942 4a8d   vst1.32 {d20-d21}, [r2]!
1c2:    ec9d 8b04   vldmia  sp, {d8-d9}
1c6:    ff62 4ee8   vcgt.f32    q10, q9, q12
1ca:    f942 6a8f   vst1.32 {d22-d23}, [r2]

当然这一切都取决于编译器,更好的编译器可以通过更清楚地使用可用寄存器来避免这种情况。

因此,如果您不使用汇编(内联、独立),或者应该不断检查编译器输出,直到您从中得到您想要的结果,那么最终您将受到编译器的支配。

【讨论】:

  • 请注意,您可以通过打开助手编辑器并从下拉列表中选择“程序集”来更轻松地获取程序集。您也可以使用 Product>Generate Output>Assembly。其中包括有用的行提示,可帮助您将程序集输出与源代码匹配(使用 otool 更难)。
猜你喜欢
  • 1970-01-01
  • 2020-08-29
  • 2017-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-15
  • 1970-01-01
  • 2012-04-07
相关资源
最近更新 更多