【问题标题】:How can I optimize conversion from half-precision float16 to single-precision float32?如何优化从半精度 float16 到单精度 float32 的转换?
【发布时间】:2011-07-27 18:50:06
【问题描述】:

我正在尝试提高我的功能的性能。 Profiler 指向内部循环的代码。我可以使用 SSE 内在函数来提高该代码的性能吗?

void ConvertImageFrom_R16_FLOAT_To_R32_FLOAT(char* buffer, void* convertedData, DWORD width, DWORD height, UINT rowPitch)
{
    struct SINGLE_FLOAT
    {
        union {
            struct {
                unsigned __int32 R_m : 23;
                unsigned __int32 R_e : 8;
                unsigned __int32 R_s : 1;
            };
            struct {
                float r;
            };
        };
    };
    C_ASSERT(sizeof(SINGLE_FLOAT) == 4); // 4 bytes
    struct HALF_FLOAT
    {
        unsigned __int16 R_m : 10;
        unsigned __int16 R_e : 5;
        unsigned __int16 R_s : 1;
    };
    C_ASSERT(sizeof(HALF_FLOAT) == 2);
    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
    for(DWORD j = 0; j< height; j++)
    {
        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
        for(DWORD i = 0; i< width; i++)
        {
            d->R_s = s->R_s;
            d->R_e = s->R_e - 15 + 127;
            d->R_m = s->R_m << (23-10);
            d++;
            s++;
        }
    }
}

更新:

拆解

; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01 

    TITLE   Utils.cpp
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
; Function compile flags: /Ogtp
;   COMDAT ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z
_TEXT   SEGMENT
_buffer$ = 8                        ; size = 4
tv83 = 12                       ; size = 4
_convertedData$ = 12                    ; size = 4
_width$ = 16                        ; size = 4
_height$ = 20                       ; size = 4
_rowPitch$ = 24                     ; size = 4
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z PROC ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT, COMDAT

; 323  : {

    push    ebp
    mov ebp, esp

; 343  :    for(DWORD j = 0; j< height; j++)

    mov eax, DWORD PTR _height$[ebp]
    push    esi
    mov esi, DWORD PTR _convertedData$[ebp]
    test    eax, eax
    je  SHORT $LN4@ConvertIma

; 324  :    union SINGLE_FLOAT {
; 325  :        struct {
; 326  :            unsigned __int32 R_m : 23;
; 327  :            unsigned __int32 R_e : 8;
; 328  :            unsigned __int32 R_s : 1;
; 329  :        };
; 330  :        struct {
; 331  :            float r;
; 332  :        };
; 333  :    };
; 334  :    C_ASSERT(sizeof(SINGLE_FLOAT) == 4);
; 335  :    struct HALF_FLOAT
; 336  :    {
; 337  :        unsigned __int16 R_m : 10;
; 338  :        unsigned __int16 R_e : 5;
; 339  :        unsigned __int16 R_s : 1;
; 340  :    };
; 341  :    C_ASSERT(sizeof(HALF_FLOAT) == 2);
; 342  :    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;

    push    ebx
    mov ebx, DWORD PTR _buffer$[ebp]
    push    edi
    mov DWORD PTR tv83[ebp], eax
$LL13@ConvertIma:

; 344  :    {
; 345  :        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
; 346  :        for(DWORD i = 0; i< width; i++)

    mov edi, DWORD PTR _width$[ebp]
    mov edx, ebx
    test    edi, edi
    je  SHORT $LN5@ConvertIma
    npad    1
$LL3@ConvertIma:

; 347  :        {
; 348  :            d->R_s = s->R_s;

    movzx   ecx, WORD PTR [edx]
    movzx   eax, WORD PTR [edx]
    shl ecx, 16                 ; 00000010H
    xor ecx, DWORD PTR [esi]
    shl eax, 16                 ; 00000010H
    and ecx, 2147483647             ; 7fffffffH
    xor ecx, eax
    mov DWORD PTR [esi], ecx

; 349  :            d->R_e = s->R_e - 15 + 127;

    movzx   eax, WORD PTR [edx]
    shr eax, 10                 ; 0000000aH
    and eax, 31                 ; 0000001fH
    add eax, 112                ; 00000070H
    shl eax, 23                 ; 00000017H
    xor eax, ecx
    and eax, 2139095040             ; 7f800000H
    xor eax, ecx
    mov DWORD PTR [esi], eax

; 350  :            d->R_m = s->R_m << (23-10);

    movzx   ecx, WORD PTR [edx]
    and ecx, 1023               ; 000003ffH
    shl ecx, 13                 ; 0000000dH
    and eax, -8388608               ; ff800000H
    or  ecx, eax
    mov DWORD PTR [esi], ecx

; 351  :            d++;

    add esi, 4

; 352  :            s++;

    add edx, 2
    dec edi
    jne SHORT $LL3@ConvertIma
$LN5@ConvertIma:

; 343  :    for(DWORD j = 0; j< height; j++)

    add ebx, DWORD PTR _rowPitch$[ebp]
    dec DWORD PTR tv83[ebp]
    jne SHORT $LL13@ConvertIma
    pop edi
    pop ebx
$LN4@ConvertIma:
    pop esi

; 353  :        }
; 354  :    }
; 355  : }

    pop ebp
    ret 0
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ENDP ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
_TEXT   ENDS

【问题讨论】:

  • 您是否尝试过在编译器上开启 SSE 优化?
  • 为什么要在联合体周围包裹一个结构体?
  • @Gregor Brandt 是的,已启用 SSE2 优化。但是这段代码会吃很多 CPU
  • @NullSet 我同意,这没有必要
  • @Thomas Matthews 已经! Profiler 指向内循环的代码。

标签: c++ performance precision sse intrinsics


【解决方案1】:

x86 F16C instruction-set extension 增加了硬件支持,可将单精度浮点向量与半精度浮点向量相互转换。

The format is the same IEEE 754 half-precision binary16 that you describe。我没有检查字节序是否与您的结构相同,但如果需要,这很容易修复(使用pshufb)。

从 Intel IvyBridge 和 AMD Piledriver 开始支持 F16C。 (并且有自己的 CPUID 功能位,您的代码应该检查它,否则回退到 SIMD 整数移位和洗牌)。

VCVTPS2PH 的内在函数是:

__m128i _mm_cvtps_ph ( __m128 m1, const int imm);
__m128i _mm256_cvtps_ph(__m256 m1, const int imm);

立即字节是一个舍入控制。编译器可以将其用作直接转换并存储到内存中(不像大多数指令可以选择使用内存操作数,它的源操作数可以是内存而不是寄存器。)


VCVTPH2PS 则相反,就像大多数其他 SSE 指令一样(可以在寄存器之间使用或作为负载使用)。

__m128 _mm_cvtph_ps ( __m128i m1);
__m256 _mm256_cvtph_ps ( __m128i m1)

F16C 非常高效,您可能需要考虑将图像保留为半精度格式,并在每次需要从中获取数据向量时进行动态转换。这非常适合您的缓存占用空间。

【讨论】:

    【解决方案2】:

    当然,访问内存中的位域可能非常棘手,具体取决于架构。

    如果您将浮点数和 32 位整数合并,并使用局部变量简单地执行所有分解和组合,您可能会获得更好的性能。这样生成的代码就可以只使用处理器寄存器来执行整个操作。

    【讨论】:

      【解决方案3】:

      这里有一些想法:

      将常量放入const register变量中。

      有些处理器不喜欢从内存中获取常量;这很尴尬,可能需要很多指令周期。

      循环展开

      重复循环中的语句,并增加增量。
      处理器更喜欢连续指令;跳跃和树枝激怒了他们。

      数据预取(或加载缓存)

      在循环中使用更多变量,并将它们声明为volatile,这样编译器就不会优化它们:

      SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
      SINGLE_FLOAT* d1 = d + 1;
      SINGLE_FLOAT* d2 = d + 2;
      SINGLE_FLOAT* d3 = d + 3;
      for(DWORD j = 0; j< height; j++)
      {
          HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
          HALF_FLOAT* s1 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 1));
          HALF_FLOAT* s2 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 2));
          HALF_FLOAT* s3 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 3));
          for(DWORD i = 0; i< width; i += 4)
          {
              d->R_s = s->R_s;
              d->R_e = s->R_e - 15 + 127;
              d->R_m = s->R_m << (23-10);
              d1->R_s = s1->R_s;
              d1->R_e = s1->R_e - 15 + 127;
              d1->R_m = s1->R_m << (23-10);
              d2->R_s = s2->R_s;
              d2->R_e = s2->R_e - 15 + 127;
              d2->R_m = s2->R_m << (23-10);
              d3->R_s = s3->R_s;
              d3->R_e = s3->R_e - 15 + 127;
              d3->R_m = s3->R_m << (23-10);
              d += 4;
              d1 += 4;
              d2 += 4;
              d3 += 4;
              s += 4;
              s1 += 4;
              s2 += 4;
              s3 += 4;
          }
      }
      

      【讨论】:

      • 现代编译器自动完成这些事情。例如。 GCC 有明确的选项,例如:-funroll-loops 和 -fprefetch-loop-arrays。要显式预取,有内置函数 __builtin_prefetch()。为此滥用volatile 是非常糟糕的建议。 YMMV。
      【解决方案4】:

      这些循环彼此独立,因此您可以轻松地并行化这段代码,无论是使用 SIMD 还是 OpenMP,一个简单的版本就是将图像的上半部分和下半部分分成两个线程,同时运行。

      【讨论】:

        【解决方案5】:

        您正在将数据处理为二维数组。如果您考虑它在内存中的布局方式,您可以将其作为一维数组处理,并且可以通过使用一个循环而不是嵌套循环来节省一点开销。

        我还将编译为汇编代码,并确保编译器优化有效并且它不会重新计算 (15 + 127) 数百次。

        【讨论】:

          【解决方案6】:

          您应该能够将其简化为使用即将推出的CVT16 instruction set 的芯片上的一条指令。根据那篇维基百科文章:

          The CVT16 instructions allow conversion of floating point vectors between single precision and half precision.

          【讨论】:

          • 谢谢,很有趣。
          【解决方案7】:

          SSE Intrinsics 似乎是一个绝妙的主意。在你走这条路之前,你应该

          • 看看编译器生成的汇编代码,(有没有优化的潜力?)

          • 搜索您的编译器文档如何自动生成 SSE 代码,

          • 在您的软件库文档(或 16 位浮点类型的来源)中搜索用于批量转换此类型的函数。 (转换为 64 位浮点也可能会有所帮助。)您很可能不是第一个遇到此问题的人!

          如果一切都失败了,那就去试试看一些 SSE 内在函数吧。为了得到一些想法,here 是一些将 32 位浮点转换为 16 位浮点的 SSE 代码。 (你想要相反的)

          除了 SSE,您还应该考虑多线程并将任务卸载到 GPU。

          【讨论】:

            【解决方案8】:

            我不了解 SSE 内在函数,但看看你的内部循环的反汇编会很有趣。一种老式的方法(可能没有多大帮助,但很容易尝试)是通过执行两个内部循环来减少迭代次数:一个执行 N(比如 32)次重复处理(循环计数宽度/N)然后一个完成余数(宽度%N的循环计数)......在第一个循环之外计算那些div和模以避免重新计算它们。如果这听起来很明显,我们深表歉意!

            【讨论】:

            • @KindDragon:为什么不编辑您的问题以包含此链接?
            • 谢谢!这个编译器输出看起来确实令人失望,不是吗?位域访问以及它所需要的所有屏蔽和移位似乎确实占据了正在发生的事情的很大一部分。似乎有优化内部循环的空间。 @Lindydancer 的建议似乎值得一试,也许自己做曲有助于产生更好的结果。
            【解决方案9】:

            这个函数只是做一些小事。通过优化来节省很多时间是很困难的,但正如有人已经说过的那样,并行化是有希望的。

            检查您获得了多少缓存未命中。如果数据正在分页进出,您可以通过在排序中应用更多智能以最小化缓存交换来加快速度。

            还要考虑宏观优化。数据计算中是否有可以避免的冗余(例如缓存旧结果而不是在需要时重新计算它们)?你真的需要转换整个数据集还是只转换你需要的位?我不了解您的应用程序,所以我只是在这里疯狂地猜测,但可能存在这种优化的空间。

            【讨论】:

              【解决方案10】:

              我怀疑此操作在内存访问方面已经成为瓶颈,并且使其更高效(例如,使用 SSE)不会使其执行得更快。然而,这只是一个怀疑。

              其他可以尝试的东西,假设是 x86/x64,可能是:

              • 不要d++s++,而是在每次迭代中使用d[i]s[i]。 (然后当然是在每条扫描线后碰撞d。)由于d 的元素是4 个字节,s 的元素是2 个,所以这个操作可以折叠到地址计算中。 (很遗憾,我不能保证这一定会提高执行效率。)
              • 删除位域操作并手动执行操作。 (提取时,先移位后掩码,以最大限度地提高掩码适合小的立即值的可能性。)
              • 展开循环,尽管像这个循环一样容易预测,但它可能没有太大区别。
              • width 沿每一行计数到零。这将阻止编译器每次都必须获取width。可能对 x86 更重要,因为它的寄存器很少。 (如果 CPU 喜欢我的“d[i]s[i]”建议,您可以进行宽度签名,改为从 width-1 开始计数,然后向后走。)

              尝试这些都比转换为 SSE 更快,并有望使其受内存限制,如果还没有的话,此时您可以放弃。

              最后,如果输出在写入组合内存中(例如,它是纹理或顶点缓冲区或通过 AGP 或 PCI Express 访问的东西,或者现在 PC 拥有的任何东西),那么这很可能会导致性能不佳,取决于编译器为内部循环生成的代码。因此,如果是这种情况,将每个扫描线转换为本地缓冲区,然后使用 memcpy 将其复制到最终目的地,您可能会获得更好的结果。

              【讨论】:

              • 顺便说一下,我假设是 x86/x64,因为 SSE 是一种 x86/x64 技术。抱歉,如果这意味着更笼统的意思...
              猜你喜欢
              • 2013-04-16
              • 1970-01-01
              • 2020-02-03
              • 2011-11-17
              • 1970-01-01
              • 2021-11-22
              • 2021-01-05
              • 2021-07-12
              • 2020-09-12
              相关资源
              最近更新 更多