clang vectorization

  1. 编译器前言

    • llvm

      • 三段式, 预处理, 字节码, 优化器汇编;

      • 这种就方便高级用户自己创建语言, 并实现对应的编译器;
    • gcc

      • 面向特定语言的编译器, 奇淫技巧, 编译优化上更专业;

    • 优化器

      • llvm更只能, 更加用户友好, 编译速度快; 且不断开发迭代;
      • gcc 用户不太友好, 但是普遍更优;
    • https://llvm.org/docs/Vectorizers.html

  2. 优化

    • 优化简介

      • innerloop vectorization 将最内层的循环使用SIMD优化, 减少循环(across multiple loop iterations), 一次尽量多的处理数据;

      • SIMD:single instruction,multiple data; 是同时对多个数据执行相同的操作, 而不是多个数据组合成一个数据再执行;

      • 如果是后者在处理浮点时就不适用; 注意区分同一个指令;各自执行同样的操作;
    • 动态检查执行

      • llvm vectorizer保留两份代码, 保存优化的循环和原始的循环, 通过动态检测来执行哪个;

      • 这种会增加一点代码量, 但是不多, 可能会减少指令命中率;
    • 参数

      • -ffast-math -O3 -msse2 -march=native -mavx2 -mss4等;

  3. 循环优化

    • 优化数量未知数量的循环

      • unrool loop: 即尽量的优化, 剩下部分使用源代码;

      void test(int *a, int len) {
       for(int i = 0 ; i < len ; ++i) {
         a[i] += 5;
       }
      }
      
      int main(int argc,char**argv) {
       int* a = new int[argc<<10];
       test(a,argc<<10);
       return a[0];
      }
      
      • 实际优化后的样子

      void test(int *a, int len) {
       int i;
       for(i = 0 ; i < len - 3 ; i += 4) {
         a[i] += 5;
         a[i + 1] += 5;
         a[i + 2] += 5;
         a[i + 3] += 5;
       }
       for(int j = i ; j < len ; j++) {
         a[j] += 5;
       }
      }
      
      • 可以看到, 最终还是会保留一部分循环;

    • 小结

      • 优化未知会保留一部分循环, 优化已知会尽量优化, 不能整除的直接完全扩张成n%w个表达式; 不会保留循环;

  4. 执行时的指针检测

    • 为什么

      • SIMD是加载若干个数据进行操作, 如果是A,B数组出现重复, 那么重复部分有先后依赖关系, 使用SIMD就会出现bug; 所以需要动态检测;

      • 动态检测, 合法就执行优化版本代码, 不合法就使用原始循环;

    • restrict

      • 修饰符告诉编译器,指针自己独占, 没有其他使用, 尽管优化; 编译器则认为独占, 就会使用其他优化手段优化; 而调用正确性就需要用户自行保证;

    • 案例分析

      void updatePtrs(size_t *ptrA, size_t *ptrB, size_t *val)
      {
       *ptrA += *val;
       *ptrB += *val;
      }
      
      ; Hypothetical RISC Machine.
      ldr r12, [val]     ; Load memory at val to r12.
      ldr r3, [ptrA]     ; Load memory at ptrA to r3.
      add r3, r3, r12    ; Perform addition: r3 = r3 + r12.
      str r3, [ptrA]     ; Store r3 to memory location ptrA, updating the value.
      ldr r3, [ptrB]     ; 'load' may have to wait until preceding 'store' completes.
      ldr r12, [val]     ; Have to load a second time to ensure consistency.
      add r3, r3, r12
      str r3, [ptrB]
      
      ldr r12, [val]  ; Note that val is now only loaded once.
      ldr r3, [ptrA]  ; Also, all 'load's in the beginning ...
      ldr r4, [ptrB]
      add r3, r3, r12
      add r4, r4, r12
      str r3, [ptrA]  ; ... all 'store's in the end.
      str r4, [ptrB]
      
      • 前后指令优化对比;

    • 小结

      • 动态检测指针, 决定执行优化版本还是普通版本;

  5. reduction variable, Induction Variables, Pointer Induction Variables

    • 解释概念

    • 案例分析

      int foo(int *A, int *B, int n) {
       unsigned sum = 0;
       for (int i = 0; i < n; ++i)
         sum += A[i] + 5;
       return sum;
      }
      
      • SIMD怎么操作sum? 同时操作? 同时操作会不会出问题?

      • 这种变量会进行优化: 拆分成若干独立的变量result = var1 op var2;, 最后按照op将拆分的result进行合并即可;

      • 一般结合-ffast-math使用;

      int foo(int *A, int *B, int n) {
       unsigned sum0 = 0;
       unsigned sum1 = 0;
       unsigned sum2 = 0;
       unsigned sum3 = 0;
       for (int i = 0; i < n - 3; i += 4) {
         sum0 += A[i] + 5;
         sum1 += A[i + 1] + 5;
         sum2 += A[i + 2] + 5;
         sum3 += A[i + 3] + 5;
       }
       return sum0 + sum1 + sum2 + sum3;
      }
      
    • 指针类型的induction

      • 这类也可以优化, 原理差不多, 动态判断是否采用优化版本循环;

      int baz(int *A, int n) {
       return std::accumulate(A, A + n, 0);
      }
      
    • 小结

      • 将看似强相关的共享变量,拆分分支,归并结果;进行优化;

      • Inductions variable: i, Reduction variable: sum;

  6. if-conversion

    • 简介

      • 尝试用算法去掉if else甚至goto; 将分支依赖变成数据依赖,即所有都执行。这样会导致任务变多,可能会变慢;

      • 参考MIT 6.172 3. bit hacks

    • 其他

      • 在一个循环中,进行计算 sum, 计数+1,偶数+2 的计算并编译优化即可反汇编看到优化;

      • 其他参考相应的 paper
  7. 反向循环

    • 说明

      • ++变成--, 这种也可以优化;

    • 其他

      int foo(int *A, int *B, int n) {
       for (int i = n; i > 0; --i)
         A[i] +=1;
      }
      
  8. scatter/gather

    • 说明

    • 案例

      int foo(int * A, int * B, int n) {
       for (intptr_t i = 0; i < n; ++i)
         A[i] += B[i * 4];
      }
      
      • 这种就是内存零散, 也是比较常见的;

  9. 混合类型的vectorize

    • 说明

      • 会评估是否有优化, 没有则不懂, 或者动态判断保留两份代码;
    • 案例

      int foo(int *A, char *B, int n, int k) {
       for (int i = 0; i < n; ++i)
         A[i] += 4 * B[i];
      }
      
      • 这种类型不一样;

  10. 常见汇编函数

    • 说明

      • 这些也可以直接进行vectorize

    • 常见

      pow exp exp2
      sin cos sqrt 
      log log2 log10
      fabs floor ceil
      fma trunc nearbyint fmuladd
      
  11. unroll when vectorization

    • 说明

      • 取决于生成文件大小和寄存器压力. 是否保留两份?

    • SLP vectorization

      • SLP: Superword-Level Parallelism
      • 在内部循环中, 自底向上找op相同, 但数据不同的零散代码进行vec操作;

      • 支持内存访问, 算数运算, 比较等都可以vectorization;

    • outer loop vectorization

      • 这种一般应用于特定语言和程序, 如OpenCL,CUDA, 这种非常依赖于外部vectorization的数据操作;

    • SLP案例

      void foo(int a1, int a2, int b1, int b2, int *A) {
       A[0] = a1*(a1 + b1);
       A[1] = a2*(a2 + b2);
       A[2] = a1*(a1 + b1);
       A[3] = a2*(a2 + b2);
      }
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值