CUDA优化的冷知识 5 | 似是而非的计时方法

2021-01-06 14:46:30 浏览数 (1)

这一系列文章面向CUDA开发者来解读《CUDA C

Best Practices Guide》 (CUDA C最佳实践指南)

大家可以访问:

https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/index.html 来阅读原文。

这是一本很经典的手册。

CUDA优化的冷知识|什么是APOD开发模型?

CUDA优化的冷知识2| 老板对不起

CUDA优化的冷知识 3 |男人跟女人的区别

CUDA优化的冷知识 4 | 打工人的时间是如何计算的

我们继续回到今天的第一个大话题, 正确的计时. 因为这话题的确很重要了, 没有了正确的计时, 一切对工作效果(代码运行快慢)的衡量工作, 都会变成虚有.

首先, 我们已经说了, 正确的逻辑顺序. 即CPU开始时刻记录->CPU发布任务给GPU->CPU等待GPU完成->CPU记录结束时刻。 这4个步骤, 任何一个步骤错误了, 都会导致错误的结果。

我们还在论坛上经常看到有人会: 启动kernel->记录开始时刻->记录结束时刻, 这样的做法也是错误的(记录开始时刻必须在启动kernel前)。特别的, 在使用非专业卡, 或者在WDDM驱动下, 该问题可能会被掩盖,即哪怕你先启动了kernel, 然后才记录开始时刻, 有的时候看起来"结果是对的", 这是因为WDDM驱动之类的有缓冲, 它可能在某种情况下会导致kernel的启动, 是被延后执行(正好插入到你记录完开始时刻后), 从而导致可能的错误的"咦, 我本次这样写也对了"的假象,从而隐藏了问题, 从而让书写者本次可能会认为, 这样写没问题的假象。我们这里严重强调一下, 必须保证正确的逻辑顺序, 才能得到正确答案。

回到今天的计时章节. 除了正确的逻辑顺序, 和工作逻辑流程上的保证(即在开始工作前计第1次时间, 必须等待工作实际完成后, 才能计第2次时间). 我们还需要对基本的计时工具本身, 进行讨论. 本实践指南手册上, 在今天这里简单的说了一下, 你应当在正确的操作系统平台上, 使用正确的计时工具/函数调用. 而我们准备详细的说一下这点, 分别对应常见的台式机上的Windows开发平台, 和我们目前在售的嵌入式上的Jetson平台(Linux)上的正确做法. 这里我们只推荐两种正确做法(也有其他的, 但这两种是推荐的).

(1) 在Windows上请使用QueryPerformanceCounter()/Frequency()这两个函数来进行计时. (技术上的理由: 它使用了主板上ACPI提供的HPET计时器, 该计时器在常见的主板上, 保证了至少几个Mhz 以上的时间分辨率, 足够)。

(2) 在Jetson嵌入式平台上, 使用gettimeofday()系统调用来进行时刻的记录. 它也同样具有很好的时间分辨率/精确性.

然后在任何一个平台上, 均不建议使用__rdtsc()或者clock()函数进行计时. 这里要重要说一下。这两个函数均是多年来, 在我们的用户中, 流行的两种方式, 可惜它们均存在一种问题(真的是好可惜. 对的选择的不多, 错误的大家都很喜欢).

我们还用上文提到的那个帖子的错误做法(https://bbs.gpuworld.cn/index.php?topic=73413.0)举例好了(没错, 除了之前的那个计时错误, 该帖子还有更多的计时错误). 该帖子是今天的实践手册上的正确做法的, 多个连续反面例子. 还是很起到很好的警示作用的.

该帖子中, 记录时刻使用了clock(). 很可惜, 该函数在不同的环境下, 有不同的返回值解释. 我们分别看一下MSDN上的clock解释, 和Linux上的man(3)手册中的解释.

首先是Linux上man手册的解释, 这是我们的jetson平台. 在该平台上, 它返回的是调用者进程所耗费的"CPU时间", 什么叫CPU时间, 和我们常用的时间有什么不同?

举个例子说, 我在CPU上执行了一个可执行文件, 上去执行了3秒, 然后突然打开了一个巨大的磁盘文件(假设我们用的是普通的机械硬盘), 或者需要从网络读取一个资源, 而突然卡顿上了10秒无响应, 然后又立刻执行了4秒. 那么此时, 它实际上使用的CPU时间是7秒(4 3), 也就是中途它卡在磁盘读取或者等待网络数据返回的时候, 那10秒虽然流逝了, 基本上是不耗费CPU的。这和我们常用概念中的实际时间(一共经过了17秒, 4 10 3=17), 是不同的.

因此你看, 虽然实际上计算了很久, 但是从"CPU时间"的角度说, 可能很短, 因此该函数和我们常用的实际生活中的时间概念是不同的, 在我们的jetson的linux平台上, 是不能使用的.

我们平常日使用的时间, 在计算机中, 叫real time, 即中文翻译是"实时钟";也叫wall time, 就是你挂载墙上的钟上经过的时间.

幸运的是, 微软的Windows平台上的clock()实现(请参考MSDN), 是返回的wall time, 也就是可用的我们的实际时间, 那么看上去, Windows上至少能用它?

实际上很可惜, 和jetson上一样, Windows也不能用. 这是为何? 因为很多客户在使用的时候, 只考虑了该计时方式的逻辑上的意义, 而没有考虑该计时方式的精度/时间分辨率. 在Windows平台上, 该函数的分辨率只有计时Hz到小于1Khz, 用人话说就是, 假设是50Hz, 它最小的分辨率只有20ms(1秒=1000ms, 分成50个周期). 而我们的代码运行的速度往往很快, 某个片段往往很短, 例如13ms, 和37ms(随意的举例), 用该函数得到的结果可能就会分别取整到了0ms, 和20或者40ms了.

此时时间都错误的离谱. 这就像我们用墙上的钟表的秒针(最长的那个指针)来计时一样, 它的分辨率只有1s级别, 如果我们的代码运行了300ms, 你会发现秒针没动, 运行时间为0;

或者我们的代码运行了1.7s, 你会发现秒针动了1下或者2下, 时间也错的离谱.

因为秒针的时间分辨率/精度不够, 所以不能用来计时。而clock()也存在类似的这个问题. 所以也不能用。

我们需要的是逻辑正确, 精度足够的计时器.

也是今天实践手册上, 在说"计时器"的选择章节, 强调的重要因素. 幸运的是, 我们的QueryPerformanceCounter()和gettimeofday()在2个平台上均可以满足这两点要求. 因此它们才变成了今天我们推荐的计时工具(CPU端, 或者你理解成老板专用工具). 此外, 今天的两个被拒绝的工具中(clock & __rdtsc), 后者也存在一处或者多处问题, 因此也不能用.

rdtsc主要是存在时基漂移(在后期的CPU和主板中逐渐的解决了), 不能跨核心同步, 以及, 还有rdtscp版本来解决其他"CPU乱序执行上"的其他问题. 这些问题或多或少的在后来的CPU/主板/操作系统中都逐渐解决了, 但是我们不敢打包票. 因此也不推荐使用.

感兴趣的读者可以进一步的自行谷歌.

好了. 回到今天的主题, 即计时的前一部分, 这主要是说的从CPU端计时, 我们后面还要说下从GPU端计时. 不过现在你已经掌握了从CPU端计时的方式了, 一般的应用应该足够了.

好了. 你已经会了CPU端计时了, 记住, 正确的计时逻辑顺序, 和使用正确的计时工具, 这两点满足了, 你就会有正确的测时结果. 我们下一篇会讲解GPU端的计时。

0 人点赞