跳转至

Libquantum

语义:用量子算法做因数分解

./libquantum 33 5

分解33, 随机种子5, 结果 33=3*11

同理,其他因数包括1397 = 11 * 127(用qemu 要跑7分钟)

400 = 4 * 100

143 = 13 * 11

按照qemu 插件获取的结果

(143 = 13 * 11)

Total Dynamic Instructions: 280089787  1

Top 10 Most Executed Basic Blocks with Instructions:

Basic Block at 0x128b0 executed 15299278 times
Instructions:
  0x128b0: 00471793          slli                    a5,a4,4
  0x128b4: 97c2              add                     a5,a5,a6
  0x128b6: 6794              ld                      a3,8(a5)
  0x128b8: 00c6f5b3          and                     a1,a3,a2
  0x128bc: 02b60e63          beq                     a2,a1,60                # 0x128f8


Basic Block at 0x128c0 executed 13065768 times
Instructions:
  0x128c0: 0705              addi                    a4,a4,1
  0x128c2: 0007079b          sext.w                  a5,a4
  0x128c6: fea7c5e3          bgt                     a0,a5,-22               # 0x128b0


Basic Block at 0x129bc executed 7060103 times
Instructions:
  0x129bc: 681c              ld                      a5,16(s0)
  0x129be: 00471693          slli                    a3,a4,4
  0x129c2: 0705              addi                    a4,a4,1
  0x129c4: 97b6              add                     a5,a5,a3
  0x129c6: 6794              ld                      a3,8(a5)
  0x129c8: 0007061b          sext.w                  a2,a4
  0x129cc: 8ea9              xor                     a3,a3,a0
  0x129ce: e794              sd                      a3,8(a5)
  0x129d0: 405c              lw                      a5,4(s0)
  0x129d2: fef645e3          bgt                     a5,a2,-22               # 0x129bc


Basic Block at 0x126a0 executed 2920476 times
Instructions:
  0x126a0: 0685              addi                    a3,a3,1
  0x126a2: 0006879b          sext.w                  a5,a3
  0x126a6: fcc7c3e3          bgt                     a2,a5,-58               # 0x1266c


Basic Block at 0x1266c executed 2919464 times
Instructions:
  0x1266c: 00469713          slli                    a4,a3,4
  0x12670: 9742              add                     a4,a4,a6
  0x12672: 671c              ld                      a5,8(a4)
  0x12674: 00b7c8b3          xor                     a7,a5,a1
  0x12678: 8fe9              and                     a5,a5,a0
  0x1267a: c39d              beqz                    a5,38                   # 0x126a0

大致看出总共110亿条指令

前两个基本块共执行6亿,分别有5,3条指令,占据48亿,大约50%执行时间

可能和我编译选项设置会不会有影响?

切片用的都是gcc 12 -O2

perf 结果

由于perf record qemu-riscv64 ./libquantum 得到的都是翻译后指令,无法查看

所以查看x86版本的

  52.02%  libquantum_base  libquantum_base.x86-gcc11.4.0-o3  [.] quantum_toffoli                                                                                                                                            
  18.34%  libquantum_base  libquantum_base.x86-gcc11.4.0-o3  [.] quantum_sigma_x                                                                                                                                            
  16.63%  libquantum_base  libquantum_base.x86-gcc11.4.0-o3  [.] quantum_cnot  

      toffoli:
0.86 18c:   mov    0x10(%r12),%r9                                                                                                                                                                                                                              
 10.75 191:┌─→mov    %rdx,%rax                                                                                                                                                                                                                                   
 10.33       shl    $0x4,%rax                                                                                                                                                                                                                                   
 10.09       add    %r9,%rax                                                                                                                                                                                                                                    
 10.46       mov    0x8(%rax),%rcx                                                                                                                                                                                                                              
 10.35       mov    %rcx,%rdi                                                                                                                                                                                                                                   
 11.10       and    %rsi,%rdi                                                                                                                                                                                                                                   
 10.83       cmp    %rdi,%rsi                                                                                                                                                                                                                                   
  0.01     │↓ je     1d8                                                                                                                              
 10.44       add    $0x1,%rdx                                                                                                                      
 10.17     ├──cmp    %edx,%r8d                                              
           └──jg     191           

这段函数占据50%执行时间,其中有两个基本块,其中第一个不跳转,第二个跳转,就是for 循环生成的。

基本块 0x128b0 (执行了15,299,278)
这是循环的主体部分
1 slli a5,a4,4: 将a4循环计数器i左移4位相当于乘以16)。
2 add a5,a5,a6: a6可能是一个基址这步计算了一个内存地址
3 ld a3,8(a5): 从计算出的地址加8的位置加载一个64位值
4 and a1,a3,a2: 对加载的值进行掩码操作a2可能是掩码
5 beq a2,a1,60: 如果掩码操作的结果等于掩码本身跳转到循环外可能是找到了目标值)。

基本块 0x128c0 (执行了13,065,768)
这是循环的增量和条件检查部分
6 addi a4,a4,1: 循环计数器i增加1
7 sext.w a5,a4: 将新的计数器值符号扩展到a5
8 bgt a0,a5,-22: 如果a0可能是循环的上限大于新的计数器值跳回循环开始

源码分析

//差不多对应内容如下
for (int i = 0; i < max_iterations; i++) {
    long long *ptr = (long long *)((char *)base_addr + i * 16);
    long long value = ptr[1];
    if ((value & mask) == mask) {
        // 执行一些操作,然后跳出循环
        break;
    }
    // 循环继续
}

// 真实源代码
for(i=0; i<reg->size; i++)
    {
        // 读取 reg->node[i].state
      if(reg->node[i].state & ((MAX_UNSIGNED) 1 << control1))
        {
           if(reg->node[i].state & ((MAX_UNSIGNED) 1 << control2))
            {
              reg->node[i].state ^= ((MAX_UNSIGNED) 1 << target);
            }
        }
    }
对上面的依赖分析可以发现依赖图!
6 影响之后的 6, 7, 1
6 -> 7
1 -> 2 -> 3 -> 4
那么横坐标代表一拍能执行的指令数目,纵坐标代表时间
2
2  2
1  2  2
1  1  2  2
   1  2  2  2
      1  1  2
         1  1
可以发现一拍最多6条指令,但是加上2条分支指令
那么在6发射处理器中,刚好能把流水线占满,ipc = 8
而在8发射中,刚好能把流水线占满,ipc = 8
目前6发射中,1拍最多占满所有指令
包括5个ALU指令(6, 7, 1, 2, 4)(add,and,slli,addi), 1个load

应该最多1拍提交8条指令吧,目前发现在 CompleT:2109888 能最多提交6条指令

20241003 满洋:如果对iteration counter a4做依赖消除,可以将ILP翻倍,按照苹果的8ALU 3LD 2ST配置,可以吃满10宽度的fetch/decode/rename。如果在decode前做指令融合,比如对1-2,4-5,后端最多能支持到每周期14条指令的吞吐量。但此时前端供指基本没有可能达到12或14。

addi.w a4, a4, 1的消除 Intel Alder Lake已实装,https://www.computerenhance.com/p/the-case-of-the-missing-increment

libquantum 6发射 vs 8发射 ipc

切片号 权重 ipc(6发射) ipc(8发射) ipc提升值
_100346 0.00731056 3.636472 5.46745 33.49%
_101164 0.00415776 5.98379 7.916369 24.41%
_14430 0.0209563 4.496246 4.743929 5.22%
_15361 0.0684355 4.261748 4.337766 1.75%
_18943 0.0232224 4.237812 4.243664 0.14%
_22269 0.0864361 4.269735 4.183884 -2.05%
_25990 0.0635782 4.43617 4.533867 2.15%
_37989 0.049903 4.460656 4.594532 2.91%
_39228 0.0140792 4.347752 4.352969 0.12%
_54695 0.0678444 4.655957 5.154718 9.68%
_58444 0.0497946 4.892461 5.000091 2.15%
_64624 0.24316 4.776473 4.773239 -0.07%
_67297 0.0188774 4.355898 5.106623 14.70%
_69779 0.0121284 4.573573 4.582266 0.19%
_72640 0.114092 4.540071 4.548779 0.19%
_96240 0.0423756 4.82169 4.974668 3.08%
_97821 0.0648098 4.409328 4.43158 0.50%
_98457 0.0488389 4.496824 4.50179 0.11%

应该再看看gem5 在运行libquantum 过程中,是否是我想的这样!

/nfs/share/zyy/spec06_rv64gcb_O3_20m_gcc12.2.0-intFpcOff-jeMalloc/zstd-checkpoint-0-0-0/cluster-0-0.json

发现101切片的ipc 最大可以到8!可惜权重太低了,对整体ipc影响不大

其中64624 这个切片权重最大,打印出其中的commitTrace

./build/RISCV/gem5.opt configs/example/kmhv3.py --generic-rv-cpt /nfs/share/zyy/spec06_rv64gcb_O3_20m_gcc12.2.0-intFpcOff-jeMalloc/zstd-checkpoint-0-0-0/libquantum/64624/_64624_0.243160_.zstd
2099898: global: system.cpu [sn:2642 pc:0x12cba] enDqT: 2059272, exDqT: 2059605, readyT: 2059605, CompleT:2060604, c_addi a4, 1, res: 0x1b1004
2099898: global: system.cpu [sn:2643 pc:0x12cbc] enDqT: 2059272, exDqT: 2059605, readyT: 2059938, CompleT:2060937, addiw a5, a4, 0, res: 0x1b1004
2099898: global: system.cpu [sn:2644 pc:0x12cc0] enDqT: 2059272, exDqT: 2059605, readyT: 2060271, CompleT:2061270, blt a5, a1, -22
2100231: global: system.cpu [sn:2645 pc:0x12caa] enDqT: 2059272, exDqT: 2059605, readyT: 2059938, CompleT:2060937, slli a5, a4, 4, res: 0x1b10040
2100231: global: system.cpu [sn:2646 pc:0x12cae] enDqT: 2059272, exDqT: 2059605, readyT: 2060271, CompleT:2061270, c_add a5, a0, res: 0x2003b13050
2100231: global: system.cpu [sn:2647 pc:0x12cb0] enDqT: 2059605, exDqT: 2059938, readyT: 2060604, CompleT:2087911, c_ld a3, 8(a5), res: 0xd8802479400000, paddr: 0xf53db058
2100231: global: system.cpu [sn:2648 pc:0x12cb2] enDqT: 2059605, exDqT: 2059938, readyT: 2061602, CompleT:2089575, and a2, a3, s0, res: 0x400000
2100231: global: system.cpu [sn:2649 pc:0x12cb6] enDqT: 2059605, exDqT: 2059938, readyT: 2088909, CompleT:2089908, beq a2, s0, 60

现在的关键点在于,bge 之前还有一条sext.w 指令,而更之前的addi 指令才是核心的递增指令!

需要更好的识别到才行!

slli a5,a4,4
add a5,a5,a6
ld a3,8(a5)
and a1,a3,a2
beq a2,a1,60
addi a4,a4,1
sext.w a5,a4
bgt a0,a5,-22  

目前这个指令序列有8条指令= 4 add + 1 sll + 2 branch + 1 load

如果在资源完全充足情况下,应该每拍能发射8条指令!

对于6发射处理器,只有4个alu, 可能导致无法完全发射

对于8发射处理器,有6个alu,2个bru,2个load, 应该可以完全发射

试试用llvm-mca 来分析下!

发现尝试了半天,目前llvm-mca, osaca 都支持x86 , aarch64,感觉如果想添加riscv64还是需要一定的工作量,我应该再看看gem5的详细结果!

如果只是运行64624 切片

似乎在运行8发射时候,ipc = 5.89

6发射时候,ipc = 4.46, 还是提升挺多的

后来发现这是warmup的ipc,应该看warmup 之后的ipc才对!

如果把所有切片全部计算, 8发射cpi=0.2158. ipc = 4.63

6发射, cpi = 0.22, ipc=4.545,提升很小!

看看commitTrace

发现运行101164 切片的ipc 已经到达8了!看看commitTrace吧

./build/RISCV/gem5.opt --debug-start=2600000000 --debug-file=libquantum.commitTrace8_101.less.gz --debug-flags=CommitTrace configs/example/kmhv3.py --generic-rv-cpt /nfs/share/zyy/spec06_rv64gcb_O3_20m_gcc12.2.0-intFpcOff-jeMalloc/zstd-checkpoint-0-0-0/libquantum/101164/_101164_0.004158_.zstd

目前发现101切片的大部分指令都不是我发现的那个热点块啊

sllw a2, a1, a5, res: 0x800000
blt a3, a2, -26
ld a5, 24(s3), res: 0x2000002010, paddr: 0x80905b38
sh2add a5, a4, a5, res: 0x200010711c
sw zero, 0(a5), paddr: 0x80e7d11c
lw a5, 8(s3), res: 0x17, paddr: 0x80905b28
c_addi a4, 1, res: 0x41444
addiw a3, a4, 0, res: 0x41444

发现646 切片也不包含我关注的热点块啊!

./build/RISCV/gem5.opt --debug-start=1500000000 --debug-file=libquantum.commitTrace8_646.less.gz --debug-flags=CommitTrace configs/example/kmhv3.py --generic-rv-cpt /nfs/share/zyy/spec06_rv64gcb_O3_20m_gcc12.2.0-intFpcOff-jeMalloc/zstd-checkpoint-0-0-0/libquantum/64624/_64624_0.243160_.zstd

还是用python脚本直接分析出来,真是特别方便哈哈哈,claude太强了,几乎直接就帮我写好代码了, 这个基本块执行50万次,占总的900万条指令的一半了

用python脚本统计commitTrace 中最常出现的基本块

python3 trace_bb.py libquantum.commitTrace8_646.less.gz > libquantum.commitTrace8_646.bb
总共900万行指令其中前两个基本块分别55万 *9 + 15 * 12 = 675
Top 10 most common basic blocks:
Count: 549857

Count: 671958 
Instructions: 3    i++
  0x12aac: c_addi a3, 1
  0x12aae: addiw a5, a3, 0
  0x12ab2: blt a5, a6, -58

Count: 671936
Instructions:  6   if 条件判断块
  0x12a78: slli a4, a3, 4
  0x12a7c: c_add a4, a7
  0x12a7e: c_ld a5, 8(a4)  8(a4)=reg->node[i]
  0x12a80: xor a2, a5, a1
  0x12a84: c_and a5, a0
  0x12a86: c_beqz a5, 38

Count: 278101  和基本块1 很像多了c_sd, lw, 但是最后的branch不相同
Instructions:  5
  0x12a88: c_sd a2, 8(a4)  if 判断对了需要store
  0x12a8a: lw a6, 4(s0)
  0x12a8e: c_addi a3, 1    i++ 
  0x12a90: addiw a5, a3, 0
  0x12a94: bge a5, a6, 34

Count: 278065  这个只是比基本块2 多了一1条load最后的branch 不同
Instructions: 7  也是if 条件判断
  0x12a98: ld a7, 16(s0)
  0x12a9c: slli a4, a3, 4
  0x12aa0: c_add a4, a7
  0x12aa2: c_ld a5, 8(a4)
  0x12aa4: xor a2, a5, a1
  0x12aa8: c_and a5, a0
  0x12aaa: c_bnez a5, -34

再看看具体的vim commmitTrace

配置了一下vim, 安装vim-quickhl 插件, 现在空格 + m 就能高亮选中区域了

感觉这4个基本块都是混合着来的,不太方便直接分析啊,并不是一个完整的循环样子?

我该如何去分析它目前为何空泡指令比较多呢?

看看objdump, 主要是quantum_cnot, 再12a78 -> 12ab2 循环执行

   12a78:   00469713            slli    a4,a3,0x4
   12a7c:   9746                    add a4,a4,a7  addr
   12a7e:   671c                    ld  a5,8(a4)  load reg[i].state
   12a80:   00b7c633             xor    a2,a5,a1  if 条件判断
   12a84:   8fe9                    and a5,a5,a0
   12a86:   c39d                    beqz    a5,12aac <quantum_cnot+0xe4> 不满足
       12a88:   e710                    sd  a2,8(a4) store
       12a8a:   00442803            lw  a6,4(s0)     load
       12a8e:   0685                    addi    a3,a3,1
       12a90:   0006879b            sext.w  a5,a3       i++
       12a94:   0307d163            bge a5,a6,12ab6 <quantum_cnot+0xee>  开头

       12a98:   01043883            ld  a7,16(s0)    load
       12a9c:   00469713            slli    a4,a3,0x4
       12aa0:   9746                    add a4,a4,a7
       12aa2:   671c                    ld  a5,8(a4)
       12aa4:   00b7c633            xor a2,a5,a1
       12aa8:   8fe9                    and a5,a5,a0
       12aaa:   fff9                    bnez    a5,12a88 <quantum_cnot+0xc0>
   12aac:   0685                    addi    a3,a3,1  i++
   12aae:   0006879b            sext.w  a5,a3  i++
   12ab2:   fd07c3e3            blt a5,a6,12a78 <quantum_cnot+0xb0>

看看对应源代码

for(i=0; i<reg->size; i++)
{   // 遍历一个大循环
    /* Flip the target bit of a basis state if the control bit is set */
    // load 一个值,比较,如果为1, xor 一下这个值
    if((reg->node[i].state & ((MAX_UNSIGNED) 1 << control)))
        reg->node[i].state ^= ((MAX_UNSIGNED) 1 << target);
}

对应的x86 perf report 汇编,时间占比

  3.24  f8:   xor     %r9,%rcx    // xor 指令                                                                                                                                                                                      ▒
            for(i=0; i<reg->size; i++)                                                                                                                                                                               
  2.82        add     $0x1,%rdx   // i++                                                                                                                                                                                    ▒
            reg->node[i].state ^= ((MAX_UNSIGNED) 1 << target);                                                                                                                                                      
  2.72        mov     %rcx,0x8(%rax)  // store                                                                                                                                                                                ▒
            for(i=0; i<reg->size; i++)                                                                                                                                                                               
  3.45        mov     0x4(%rbp),%esi  // esi = reg->size                                                                                                                                                                             ▒
  2.65        cmp     %edx,%esi        // i < esi ?                                                                                                                                                                             ▒
             jle     129          // 跳出循环                                                                                                                                                                                    ▒
  2.83 10a:   mov     0x10(%rbp),%rdi                                                                                                                                                                                
            if((reg->node[i].state & ((MAX_UNSIGNED) 1 << control)))                                                                                                                                                 
 13.33 10e:┌─→mov     %rdx,%rax                                                                                                                                                                                      
 11.72       shl     $0x4,%rax                                                                                                                                                                                      
 10.36       add     %rdi,%rax                                                                                                                                                                                      
 12.16       mov     0x8(%rax),%rcx  // load                                                                                                                                                                               ▒
 14.00       test    %r8,%rcx        // 判断                                                                                                                                                                                 ▒
           │↑ jne     f8              // 满足,执行f8 xor 指令,然后继续循环                                                                                                                                                                              ▒
           for(i=0; i<reg->size; i++)                                                                                                                                                                               
 10.03       add     $0x1,%rdx       // 不满足,i++, 继续循环开头                                                                                                                                                                               ▒
 10.68     ├──cmp     %edx,%esi                                                                                                                                                                                      
           └──jg      10e

看来这个riscv 汇编应该是高度优化后的汇编代码,就是两个基本块有所变异

那么按照次数来说,进入if 条件内应该比较少,所以核心代码应该还是前两个基本块

目前loop buffer 只是对单个小循环不断重复的情况做的恨到

但是对这种两个循环交替执行的情况,if 条件内可能进去,可能不进去,做的不好,所以导致前端供应指令不够,每次之能取到4-6条指令,在8发射情况下做的不好,需要bi-taken 分支获取技术。

topdown 全局分析

frag stall 来看, 总指令数目2000万

fetchStallReason::NoStall     24786497
fetchStallReason::FragStall     39688863  3千万
decodeStallReason::NoStall     3109 2223
decodeStallReason::FragStall      1130768 100
renameStallReason::NoStall     2004 2996
renameStallReason::FragStall     1217 5302   1200
dispatchStallReason::NoStall     2004 1608
dispatchStallReason::FragStall     1218 9184  1200

    StallReason::total      3352 0216

也是fetch stall 特别大