摘要

从最开始得需求开始分析,进而产生功能,基于功能设计软件架构,然后基于架构选择合适的软件方案进行系统的设计,可谓是保姆式设计了,带你手把手的开发跨平台的开源库!塞班~

前言

  • 上一篇给出了大致的框架如下图所示,我并没有具体到软件行为中该如何设计等细节,因此本篇,我将在提供架构的基础上、软件的角度出发,设计软件架构,并详细分析每一个细节部分的软件设计,大概包括这些方面:
    • 如何按功能设计分块?
    • 如何选择C++的设计方案?
    • 如何设计Cmakelist?
    • 如何设计底层接口?
    • 如何在JNI的环境下内嵌汇编?
    • 等等

Arch


CPU partial

CPU部分可以参见之前的知乎专栏:


零 · 需求

首先我们收集下需求,大致是:

  • 函数级perf(单case测试)
  • 代码片段级别perf(在框架中需要fine tune某一部分核心代码)
  • 硬件参数perf(我们需要获取硬件参数比如主频,内存latency,FLOPS, 指令吞吐,发射能力等等等)
  • 支持多平台兼容(CPU/GPU/XPU,ARM/X86/RISC-V, Android/IOS/linux/Windows)

壹 · 顶层接口设计

1.1 perf code行为

我们知道需求了,因此下一步就是把这些行为进行抽象,然后用尽可能简单的架构来实现,我们发现行为就是{TICK(events[]);TOCK(event[]);}组合,

operator(){
  TICK(const &events[])
  // code part.
  TOCK(const &event[])
}

在不同的平台下有不一样的实现,但是行为都是一样的,于是我们很自然的就想到了虚函数实现;

但是虚函数就能完全解决问题嘛?你想下,假如我现在支持CPU,GPU,发布了代码,后续支持XPU了,将面临至少两个方面的问题,首先是vendor不给底层细节的尴尬处境,然后就是我们每次更新都需要重新编译整个工程的麻烦过程,这么整是不是贼麻烦?

假如我们采用热插拔的概念来设计架构就能比较巧妙的一定程度上避开这个问题了,用plugin的方案保证core部分的代码基本不变,调用动态库的方式加载XPU的底层实现,这部分是由vendor提供的,或者我们自己按照X-profiler接口包装的,其次每次更新支持的时候我们都不需要更新核心代码,只需要更新.so/.dll文件即可,“王德发”~

到此我们就知道整体是plugin模式(纯虚函数)来实现了,再具体点到数据格式,我们知道每个平台的events是不一样的,因此events也是要重载的,于是流程上我们需要一个setup函数,来获取events的数据的版本。

cmake-examples

1.2 获取硬件info

你看我们的算子构成是这样的:

operator(){
// code 前处理
// 循环最内层asm汇编部分
//code 后处理
}

我做过大量的实验,在各种算子的实现方式下,“循环最内层asm汇编部分”占了算子总耗时90%+(除了某些特定的算子),因此矛盾的主要点就在这个汇编算子呀!

但是发现问题点没,这部分需要重点优化的就是某段汇编代码,我们需要实现一种灵活方便且相对精确的perf工具来在线perf这段代码块的性能参数!


贰 · perf要得到的参数

如何优化?

循环展开、并行、指令流水重排、cache优化、减少内存复用。。。总共也就这些呀!

但是我们通常就是靠经验来优化一些代码呀,并没有很好的性能参数量化指导方案呀!因此我们可以这么设想:

“要是我写了某一段代码,然后在开发project内编译一跑,就直接得到一组硬件性能参数,并告诉你这段代码只利用了硬件资源的百分之多少,还有多少的上升空间,需要改进的方面有哪哪哪几个方面!”

这样无论是对于理解硬件特性还是优化代码性能,都是一个很不错的设想呀~

嗯就从这个出发,我们可以大概分这么两大类,instruction + cache:

  • 指令流水线这块主要是获取IPC参数,需要的性能参数有指令数、CPU cycles数;
  • cache这块就主要关注:Lx cache hit, Lx cache miss,writeback…..;

要的东西列出来了,那么接下来的问题就是去哪获取的问题了,这个好办,看芯片手册嘛!

ARM PMU function

你看这个模块叫做PMU,你的代码在CPU硬件上跑的过程中就会被这个模块给默默地记录下来了!是硬件级别的记录啊,从我们软件er的角度来说可以认为是实时获取的。

好了知道从哪获取了那我们接下来要考虑的就是如何获取了!

如何获取?我就想到了在前公司使用过的DS-5,当时用它在裸板上调试代码,能看到超多的硬件实时参数信息,我想着我是不是也可以仿照做个阉割版。。。。

直接上官网一看:

里面有段介绍:

得了,这下清楚了,总结下来实现方案就是:

代理:收集数据 → 传输数据;

接下就是好玩的部分了,我们可以凭空想象,想怎么实现就怎么实现,任何技术任何想法,这种“创造”的感觉太TM爽了!咳咳,来,咋们还是再扯扯具体怎么实现吧!


叁 · perf应用框架系统搭建

总之想了几天之后,实现的技术框架就是这个样子,当然里面还是有很多细节的,比如究竟如何获取PMU里面的数据,也是碰到了好多的坑,一步一步才填过来的!这里只做介绍,细节暂且不表,以后有机会再细说实现代码呀!

是吧,思路超简单呀,分两大块上位机显示界面加移动端数据提供端,中间通过自定义的简单通信协议进行数据传递。

最终的实现效果如图所示呀!还是蛮粗糙!但也凑合着用了哇~


肆 · perf工具精度验证

自己写了得验证精度不是,不然咋知道我测的数据准不准啊!

反正一顿操作得到一组数据:

实测数据及结论

理论访存次数 = 4000000
CPU_CYCLES = 341089056
L1D_CACHE_REFILL = 4014193
L1D_CACHE = 4000188
LD_RETIRED = 4000120
Average latency = CPU_CYCLES/LD_RETIRED=85 cycles

L1D_CACHE_REFILL:
就是cache miss数,由于每次访存都miss了,因此,总共是4000000+系统扰动约等于4014193,误差为0.3%;
LD_RETIRED:
访存指令数,理论值为40000000,测试误差为0.03‰;
L1D_CACHE
为CPU实际访问cache的次数,误差为0.05‰;

注意这里我就测了个普通场景的精度,因此在一定的测试环境下精度还是可以的,严苛场景的话就需要我们做一些转换啦~


伍 · 举两个栗子

首先就是举个指令流水的例子,我们的例子是线性访存,如右图所示:

按图所示访存,出现了两个阶梯,第一个阶梯是平均latency为1 cycles,第二个阶梯为2 cycles,第三个就稍微复杂些了,属于cache的范畴,暂且不表,我们来分析下第一二个阶梯的区别及产生的原因;

第一二个阶梯的区别就是一个加了地址偏移0,另外一个没加,因此就是累加地址偏移值的这个操作需要花费一个cycles,从而导致指令耗时增加,我们可以画了流水图看一下:

可以看到理论值跟实测值还是蛮吻合的嘛~

第二个例子就是可以构建出特定硬件的存储器山三维图呀,我们先不看三维就看个二维的,也就是说获取某个特定硬件的L1 latency,L2 latency, memory latency呀~

第三个例子就是在写算子过程中,发现某些特定size下性能的缺陷,通过这个工具可以轻松定位到是cache way conflict的问题呀!(当然,经验丰富的高工前辈们还是能一眼就看出来的呀!向前辈们学习!)


陆 · 总结

总之,这篇文章我实现希望告诉大家我大概做了个啥,能干啥(辅助调优算子)!为下一篇博文做铺垫而已呀! 因此就在这草草搁笔啦!当然我还是希望今后能有机会把这个工具开源出来跟大家一起学习摸索呀!
笔心~

所以当初自己挖的坑,现在终于想起来要填啦,欢迎大家关注呀AI-performance呀!

GPU partial

XPU partial

点击下面找寻彩虹~

🌈 获取中...

-->

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

X-profiler startup Arch. 下一篇