Q :在这样的设置下,如何测量每个线程的实际计算时间?
一个简单的半手动代码执行时间分析的简单模拟代码:
毋庸置疑,对于“嘈杂”的执行平台,选择CLOCK_MONOTONIC
保存漂移时间更新,但不会“保存”非 CPU 核心等待状态,因为任何(如果重,则更多)“背景“-(令人不安的)-由操作系统调度的进程。
然而,对于原型设计阶段,这比安装所有“omp-native”回调更
{ ompt_callback_task_create_t, ompt_callback_task_schedule_t, ompt_callback_task_dependence_t, ompt_callback_dispatch_t, ompt_callback_sync_region_t, ..., ompt_callback_thread_begin_t, ompt_callback_thread_end_t, ... }
容易处理。
副作用奖励:
如果报告和后处理记录的嵌套代码执行相应的持续时间,那么简单代码允许“框架”相关调用签名和递归嵌套相关开销的隐藏成本。
修改后的、开销严格的阿姆达尔定律然后停止欺骗你并开始更准确地向你展示,当这段代码开始失去与开销相关的(加上由于潜在的工作单元原子性)主要-[SERIAL]
任何预期真实的附加成本-[PARALLEL]
部分加速(预期利用更多(那些并且只有那些否则免费的)资源)。
这始终是战争中最艰难的部分(仍需继续战斗……)。
EFFICIENCY of SCHEDULING & OCCUPIED RESOURCES' of a CALL to 2-ary task-SCHEDULED fun() with hidden 1-ary RECURSION:
CALL
42----*--------------------------------------------------------------------------------------*
: | |
: | 21----*---------------------------------------*
: | : | |
: | : | 10----*----------------*
: | : | : | |
: | : | : | 5----*----*
: | : | : | : | |
: | : | : | : | 2<
: | : | : | : 2< /
: | : | : 5----*----* 5___/___/................ #taskwait 2
: | : | : : | | /
: | : | : : | 2< /
: | : | : : 2< / /
: | : | : 5___/___/ /
: | : | 10___/____________/............................. #taskwait 5
: | : 10----*----------------* /
: | : : | | /
: | : : | 5----*----* /
: | : : | : | | /
: | : : | : | 2< /
: | : : | : 2< / /
: | : : 5----*----* 5___/___/ /
: | : : : | | / /
: | : : : | 2< / /
: | : : : 2< / / /
: | : : 5___/___/ / /
: | : 10___/____________/__________/.......................................................... #taskwait 10
: | 21___/
: 21----*---------------------------------------* /
: : | | /
: : | 10----*----------------* /
: : | : | | /
: : | : | 5----*----* /
: : | : | : | | /
: : | : | : | 2< /
: : | : | : 2< / /
: : | : 5----*----* 5___/___/ /
: : | : : | | / /
: : | : : | 2< / /
: : | : : 2< / / /
: : | : 5___/___/ / /
: : | 10___/____________/ /
: : 10----*----------------* / /
: : : | | / /
: : : | 5----*----* / /
: : : | : | | / /
: : : | : | 2< / /
: : : | : 2< / / /
: : : 5----*----* 5___/___/ / /
: : : : | | / / /
: : : : | 2< / / /
: : : : 2< / / / /
: : : 5___/___/ / / /
: : 10___/____________/__________/ /
: 21___/_______________________________________________________/...................................................................................................................... #taskwait 21
42___/
RET_/
效率
调度和占用资源的调用 2-ary task
-SCHEDULEDfun()
与隐藏的 1-ary RECURSION 对于任何不断增长的规模workload
变得越来越重要workload < someBound * 2 ^ W
,但代价是非常高的W
(这会导致W * k
-ary-many 次重新-{-acquired, -allocated, -released}浪费了所有k
请求#pragma omp task shared(...)
的处理相关资源,在整个纯[SERIAL]
定义递归潜水和重铺返回的过程中。
很容易看出有多少资源会挂起等待(由于甚至是 1-ary RECURSION 公式),直到每一次深入递归的最深层次,都会冒泡回到顶层#pragma omp taskwait
。
为每个递归潜水级别重新分配新资源和新资源的成本通常会在开销严格的阿姆达尔定律(性能方面)上杀死你,如果没有陷入颠簸或与系统配置相关的溢出,因为破坏了真实的- 更早的系统物理资源,用于任何相当大的递归深度。
如果不使用“典型的便宜”但昂贵的(空闲/浪费)资源递归问题公式,即使使用最轻量级的一元案例,这些都是您不需要支付的成本。
看看有多少-:
表示的“等待线”并行存在,除了拓扑的任一阶段中有多少-|
表示的“计算线”,浪费/阻塞,但必须让所有与任务相关的资源(内存和堆栈空间只是比较明显的那些,它们在性能方面非常昂贵(只是让大部分处理时间空闲等待),或者如果超额订阅超出真实系统的配置容量,则容易由于溢出而崩溃)。
战争是你的!继续走 ...
网站合规免责声明:
-------------------------------------------- ----------------------------------
根据 StackOverflow 政策,此处发布了完整的模型代码,用于在任何情况下,Godbolt.org平台都可能无法访问,否则请随意选择和/或使用Compiler Explorer工具,这远远超出了放入模型源代码中的字符序列
选择 &执行它的乐趣永远是你的:o)
#include <time.h>
int estimateWorkload() {
return 42; // _________________________________________________________ mock-up "workload"
}
int serial_a_bit_less_naive_factorial( int n ){
return ( n < 3 ) ? n : n * serial_a_bit_less_naive_factorial( n - 1 );
}
int serialFunction() {
return serial_a_bit_less_naive_factorial( 76 );
}
int parallelFunction( int workload, const int someBound ) { // __ pass both control parameters
struct timespec T0, T1;
int retFlag,
retValue,
result1,
result2;
retFlag = clock_gettime( CLOCK_MONOTONIC, &T0 ); // \/\/\/\/\/\/\/\/\/\ SECTION.begin
if ( workload < someBound ) {
retValue = serialFunction();
}
else { // -- [SEQ]----------------------------------------------------
#pragma omp task shared( result1 ) // -- [PAR]|||||||||||||||||||| with (1-ary recursions)
{
result1 = parallelFunction( (int) workload / 2, someBound ); // (int) fused DIV
}
#pragma omp task shared( result2 ) // -- [PAR]|||||||||||||||||||| with (1-ary recursions)
{
result2 = parallelFunction( (int) workload / 2, someBound ); // (int) fused DIV
}
#pragma omp taskwait
retValue = result1 < result2;
}
retFlag = clock_gettime( CLOCK_MONOTONIC, &T1 ); // \/\/\/\/\/\/\/\/\/\ SECTION.end
// ____________________________________________________________________ MAY ADD ACCUMULATION (1-ary recursions)
// ...
// ____________________________________________________________________ MAY ADD ACCUMULATION (1-ary recursions)
return retValue;
}
int main() {
const int someBound = 3; // _______________________________________ a control parameter A
#pragma omp parallel
{
#pragma omp master
{
// -- [SEQ]---------------------------------------- do some serial stuff
// ------------------------------estimate if parallelisation is worth it
const int workload = estimateWorkload();
if ( workload < someBound ) {
serialFunction();
}
else {
parallelFunction( workload, someBound ); // -- [PAR]||||||| with (1-ary recursions)
}
}
}
}