# swgcc710-cxx **Repository Path**: swmore/swgcc710-cxx ## Basic Information - **Project Name**: swgcc710-cxx - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 0 - **Created**: 2021-04-22 - **Last Updated**: 2023-03-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ************************** 手里有家伙了,咱腰杆子就硬 ************************** 记一次使用神威·太湖之光计算1+2=3的经历 前言 ---- 大概是去年的时候,终于从机器的制造商那里拿到了swgcc710。当我第一次用-mslave编译从核C++程序的时候,惊喜地发现成功生成了一个.o文件,从此开始了C++写神威从核的不归路。 从一开始想办法让主核端找到从核符号从而启动线程,到大概今年1月,用C++重构完的bio-ESMD从核竟然比C版快了一丢丢。 Round 0 如何让主核代码找到从核函数 ---------------------------------- 一开始虽然两端的C++代码都能成功编译,但是,链接不起来,分析两边生成的符号,就会发现,主核使用 `extern void slave_add(void*)` 时生成了类似这样的符号查找:: 0000000000000000 *UND* 0000000000000000 _Z9slave_addPv 而从核端定义这个函数生成的符号是:: 0000000000000000 g F .text1 0000000000000010 0x80 slave__Z3addPv 把符号名字对齐来看,我们发现:: _Z9slave_addidPd slave__Z3addidPd 好吧,从核编译器会往函数前面加 `slave__` ,主核编译器会把你的 `slave_` 给mangle起来。 那我不管你C++,主核就 `extern "C" void slave_add(void*)` ,从核就:: extern "C" { void add(void *){ 函数体 } } 两边终于生成了相同的符号: `slave_add`。就这样,我凑合着用了小半年,bio-ESMD的SHAKE都上从核了。也就这样。如果遇到模板函数,那么就在外面再包一层C函数让主核去找:: template void add(T a); extern "C" { void add_double(void *arg){ double* darg = (double*)arg; add(darg[0]); } } 用倒是也能用。时间长了,也就不想忍了。 Round 1 swgcc的GLD -------------------- swgcc710的从核函数调用永远会生成基于GP的跳转,这是一件非常让人不爽的事情。为了解决这个问题,我们有一套上古流传下来的sw5cc短跳转改拼接常量+长跳转的后处理程序,先生成汇编,然后汇编里把短跳转换成常量拼接并添加一个符号。类似于把这样的汇编:: br ra, xxxx+8 !braddr //跳转到xxxx函数,下一条指令地址压入ra寄存器 改成:: usesym.xxxx.一串胡乱生成的UUID: //插入的符号 ldih t12, 0($31) ldi t12, 0(t12) br ra, xxxx !braddr //保留对xxxx的依赖避免链接器删除该符号,后续会换成sll t12, 32, t12 ldih t12, 0(t12) ldi t12, 0(t12) call ra, t12, 0 本来链接器是找到 `br ra, xxxx !braddr` 这样的指令填入真实的偏移量替换 `xxxx`,我们这样程序安然链接过后就去找 `usesym.xxxx.一串胡乱生成的UUID` 然后去修改那一堆立即数拼接的偏移量。 swgcc710虽然改成了GP跳转的方式,类似:: ldl t12, xxxx(gp) !literal //把从gp+某偏移量读出xxxx的地址 call ra, t12, xxxx !lituse_jsr //跳转偏移量提示(用于提升流水性能) 但我们仍然还能用这样的思路。 但是,**注意**,sw5cc的时候这样做是为了避免调用C库函数时短跳转后大应用里GP值不对的问题,但是swgcc是为了干掉GLD,同时,C库函数里面的GP跳转,后处理汇编的方式就力有未逮了。bio-ESMD最初的解决方案是不调C库,永远造轮子。 Round 2 我能不能不后处理汇编 ---------------------------- 后处理汇编的两大弊端:1. 编译变慢,时间就是生命,我浪费我的时间,谋我的财,害我的命; 2. 太容易生成BUG了。但是直接在.o或者编译好的ELF文件里插入指令相当困难。不插入指令,就要想一条指令的解决方案。 最早的尝试是对ELF直接后处理,把读GP的指令nop掉,然后call换成br,结果,我跟我们拿到的的带relax的链接器一样,翻车翻到沟里去了。 Round 2.1 .got1 ^^^^^^^^^^^^^^^ 我想,就算 `.got` 超过了64KiB的程序,其实主要也就是主核用到的 `.got` 大,从核函数都是人写的,你能写多少?所以我们的下一个考虑是把从核用到的 `.got` 段数据抽出来,放到LDM里。这样就一条不带GLD的指令就读出目标地址了。 我是实现了这个功能的,大概就是: 1. `-Wl,-q` 保留relocs 2. 对 `.text1` 段的relocs进行合并(其中处理static符号依赖的代码写了不下30个bug费了两天),生成一个假的 `.got` (符号`__got1`)塞到 `.ldm` 段(也就是以前的 `__thread_local`)。 3. 对所有函数开头和函数调用返回后的GP处理指令进行替换,将GP指到 `.ldm` 段。 我成功把 GP 指到了 `__got1`,但是:: 0000005000004040 g .ldm 0000000000000100 __got1 //“手术很成功” 也就是,新的LDM静态变量的实现好像是 `.ldm` 段在主存,然后加载器负责拷贝到LDM上。 Round 2.2 救我!!! ^^^^^^^^^^^^^^^^^ 没有费特别大的功夫,我找到了 `.ldm` 段到LDM地址的映射方式,按理说可以改GP的值了。但是,除了直接基于GP跳转的 `!literal` 重定位,还有基于GP进行32bit偏移的 `!gotprel` 重定位。我把GP从 `0x5000000000` 拉到 `0x100` 左右的话,肯定跳不回去了 `T_T`。 于是继续用了三个月的汇编后处理程序。 Round 2.3 反杀!!! ^^^^^^^^^^^^^^^^^^ 在一个喝着咖啡写论文的下午,突然写到SW26010的LDM只有64KiB,突然脑补“也就是`0+` 一个16位的偏移量可以跳转到LDM的任何地方”,那么我把GP指到LDM图个啥呢?我为啥不: ldl t12, xxxx(zero) 从常0寄存器把这个偏移搞定,GP还放在原来的地方就好了。后来,为了降低 `__got1` 的LDM开销,毕竟LDM 1nm土1寸金,就采用先链接一次,估计好这个 `__got1` 的大小,生成一个 `.o` 再与程序的其他部分链接。这样需要链接两轮,但是脚本分享出去的话至少不用告诉用户“你要在从核上定义一个__thread_local long __got1[256]的变量”了(因为大家 **从来不看文档,甚至也不看程序报错的提示内容** ,我除非实现在编译节点上播放声音)。所以最后的产物是一个链接器wrapper而不是一个链接后处理程序(其实倒也差不太多) Round 3 都两轮链接了? ---------------------- 既然我要链接两轮,我是不是顺便在第一轮链接里找出来主核上找不到的类似 `_Z9slave_addPv` ,第二轮链接的时候替换就好了?所以,后来我在脚本里又加上了这一块的处理:识别类似 `_Z(\d+)slave_(\w+)` 的变量,并在第二轮链接的时候用 `--defsym` 提示编译器?想法是好的,实现也很愉快。对应的Python代码大概就是:: MANGLE_RE = re.compile(r"_Z(?PN?)(?P\d+)slave_(?P.*)") def remangle_slave(mangled_name): m = MANGLE_RE.match(mangled_name) if m: remangle = "slave__Z%s%d%s" % (m["ns"], int(m["len"]) - 6, m["rest"]) return remangle else: return None 嗯,再加上一个97行的ELF解析模块,40行的reloc识别模块,和一个6+3行的指令识别模块,这个功能就算是实现了。其中ELF解析模块参考了 `linux/elf.h` (当年 `SWLU` 的内置符号表解析也是照着这个头文件实现的), reloc识别模块参考了GNU的链接器里面对reloc解析的代码加上 `sw5readelf` 出来的内容反推了reloc类型。至于指令,其实是用汇编器汇编如下指令并按注释中的方式分析二进制:: ldl $0, 0($0) //1 ldi $0, 0($0) //2 ldih $0, 0($0) //3, 1~3三条指令对比找出ldl,ldi,ldih的操作码 ldl $0, -1($0) //4, 与1对比寻找偏移值对应的位,盲猜偏移值与机器一样是小端 ldl $31, 0($0) //5, 与1对比寻找目标寄存器对应的位 ldl $0, 0($31) //6, 与1对比寻找基址寄存器对应的位 这样就搞定了主从C++换名的处理,也搞定了从核GLD的消去。 Round 4 C++真香 --------------- 笔者作为一个8年内只用纯C,必要的时候学一下Fortran搞高性能计算的人,才转型到C++,手里有把迫击炮,看谁都像坂田老鬼子,就举一个诡异的C++用法吧,C++11中引入了tuple,可以用来存放异质的数据,例如 `a = std::make_tuple(1, 2.0)` 生成一个 `tuple` 类型,可以用 `std::get(a)` 取出a的第idx个元素。C++17中又引入了 `std::apply(function, tuple)` 将 `tuple` 中存放的数据作为参数列表调用 `function` 。那么,如果我们实现: 1. 主核端将参数打包成 `std::tuple` 2. `athread_spawn` 一个从核代理函数,它接受一个 `std::tuple*` 作为参数 3. 从核代理函数调用 `std::apply`,启动从核函数。 岂不就能实现 `athread_spawn(slave_test, a, b)` 这种功能了?于是我考虑动手,首先,变长参数列表永远用parameter pack而不是 `va_arg`,主核的打包并启动代码:: extern "C"{ #include } #include //从核的代理调用函数 template extern void slave_tuple_spawn_proxy(std::tuple*); template void athread_spawn_tupled(void (*f)(Ts...), Ts ...args) { //想了想好像要把函数指针一起打包进去 auto arg = std::tuple(f, args...); //注意:就算是过去的athread,也是SLAVE_FUN(f)将函数展开成slave_f,而athread_spawn(f, *arg)是个宏,展开成__real_athread_spawn(slave_f, arg) //所以这里用__real_athread_spawn其实对我来说清晰一些 __real_athread_spawn((void*)slave_tuple_spawn_proxy, &arg); } 这里想了想,原来的 `athread_spawn` 只接收一个参数,那么就要把要调用的函数一起打包到 `tuple` 里面。(其实给后续的编程带来了很大的困扰) 从核采用了分级实现的方式, `tuple_spawn_proxy` 的主要任务是把 `athread_spawn` 收到的 `std::tuple*` 给取到LDM里面来,此外,这里这样写的一个关键原因是这里的主核的模板是 `template `,而从核的模板是 `template ` 的话,主从模板参数不一样,导致name mangling也不一样,最后就主从核失之交臂了,所以,把主核的 `extern` 也改成了 `template extern void slave_tuple_spawn_proxy(std::tuple*);`,从核这样实现:: template __attribute__((noinline)) void tuple_spawn_proxy(std::tuple *arg) { std::tuple ldm_arg; dma_getn(arg, &ldm_arg, 1); std::apply(call_tuple, *arg); } DMA得手后就直接把 `tuple` 给一个叫 `call_tuple` 的函数,这个函数负责真正启动从核计算函数:: template __always_inline void call_tuple(void (*f)(Ts...), Ts ...arg){ f(arg...); } 其实这个函数最大的作用是在它的模板里,已经成功把函数 `f` 和参数 `arg` 分拣了开来,这样就可以调用真正的从核函数了。 编写简单的测试代码,主核:: #include "tuple_spawn.hpp" #include extern void slave_add(int a, double b, double *c);//*c = a+b int main(){ athread_init(); double c; athread_spawn_tupled(slave_add, 1, 2.0, &c); athread_join(); printf("%f\n", c); } 从核:: #include "tuple_spawn.hpp" extern int _MYID; void add(int a, double b, double *c) { *c = a + b; } 编译报错:: --defsym:1: undefined symbol `slave__Z17tuple_spawn_proxyIPFvidPdEJidS0_EEvPSt5tupleIJT_DpT0_EE' referenced in expression 这个原因是C++的模板是按照需求实例化的,没有对 `tuple_spawn_proxy` 的调用就不会生成对应的代码,为了绕过这个问题,考虑设计一个强行依赖的方案:: template void tmpl_entrance(void (*f)(Ts...)){ __asm__ __volatile__ ("" : :"r"(tuple_spawn_proxy) : "memory"); } #define ATHREAD_VISIBLE(x) template void tmpl_entrance(decltype(x)*); 这里我们用内联汇编欺骗编译器说 `tuple_spawn_proxy`,也就是 `f` 所对应的代理函数会被用到,然后定义宏 `ATHREAD_VISIBLE(x)` 来强行实例化 `tmpl_entrance(decltype(x)*)`,从而使得 `tuple_spawn_proxy` 也被实例化,从而可以让祝贺找到。这样从核代码里只要再加一句:: ATHREAD_VISIBLE(add); 编译就成功了。程序运行结果显然是:: 3.00000 此外,由于 `std::apply` 是C++17引入的特性,swgcc710虽然支持C++17,但是可能有些项目还是再用C++11或者C++98,为了降低对新特性的依赖,我还去抄了一个C++11可以用的`apply`回来:: namespace cxx11_apply { template struct integer_sequence { }; template struct GenSeq : GenSeq { }; template struct GenSeq<0, Is...> { typedef integer_sequence type;//GenSeq::type就是integer_sequence<0,1,...,N-1> }; template void __apply(Tt t, Tf f, integer_sequence seq)//这样从integer_sequence可以让编译器推导出Is... { f(std::get(t)...);//就会展开成 f(std::get<0>(t), std::get<1>(t)...),完成函数调用 } template void apply(void (*F)(Ts...), std::tuple tuple) { typename GenSeq::type a;//a的类型就是integer_sequence<0,1,...,N-1> __apply(tuple, F, a); } } 原理不太难,但是想明白它用了我好长时间。这样在C++11的条件下我们也可以这样spawn了。 写在最后 -------- 如果说sw5cc是一个值得打59.999分的编译器,那么swgcc710应该能打到85分左右,剩下的问题主要是从核函数调用生成GLD的问题和部分Fortran特性性能不高。可以预见在未来的几年内swgcc系列可能为神威代码的现代化做出重要的贡献。