如何使用 OMP 任务和递归任务工作负载测量 OMP 时间?
How to measure OMP time with OMP tasks & recursive task workloads?
下面我尝试绘制使用 OpenMP 任务并行化的代码。
在 main 函数中,一个并行环境被启动,之后代码立即被包装到 #pragma omp master
部分。在计算出预期的工作量并根据此工作量是否低于给定阈值后,必须完成的工作将传递给串行函数或递归地拆分工作量并初始化单独任务的函数。然后在 #pragma omp taskwait
指令之后汇总每个 #pragma omp task
的结果。
int main() {
#pragma omp parallel
{
#pragma omp master
{
//do some serial stuff
//estimate if parallelisation is worth it.
const int workload = estimateWorkload();
if (workload < someBound) {
serialFunction();
}
else {
parallelFunction(workload);
}
}
}
}
int parallelFunction(int workload) {
if (workload < someBound) {
return serialFunction();
}
int result1, result2;
#pragma omp task shared(result1)
{
result1 = parallelFunction(workload/2);
}
#pragma omp task shared(result2)
{
result2 = parallelFunction(workload/2);
}
#pragma omp taskwait
return result1 < result2;
}
在这样的设置下,如何衡量每个线程的实际计算时间?
如果我测量 CPU 时间并且有 k
个活动线程,那么我将得到 k*wallTime
,这是有道理的,因为线程是由前导 #pragma omp parallel
指令初始化并始终保持活动状态。然而,这并没有给我任何关于线程实际工作花费多少时间的信息,这使得代码难以分析。
Q : How do I measure the actual computing time of each thread in such a setting?
一个简单的 MOCK-UP CODE 用于简单的半手动代码执行时间分析:
不用说,对于“嘈杂”的执行平台,选择 CLOCK_MONOTONIC
可以节省漂移时间更新,但不会“节省”off-CPU-核心等待-状态,由于 O/S.
安排的任何(如果很重)“背景”-(令人不安的)-进程
然而,对于原型设计阶段,这比安装所有“omp-native”callbacks'
{ 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, ... }
处理要容易得多。
副作用奖励:
如果报告和 post 处理记录的嵌套代码执行各自的持续时间,那么简单的代码允许“构建”相关调用签名和递归嵌套相关开销的隐藏成本。
然后 revised, overhead-strict Amdahl's Law 不再撒谎并开始更准确地向您展示,当此代码开始丢失时 在与开销相关的(加上由于潜在的 工作单元的原子性(s) ) 原则上-[SERIAL]
-任何预期 True-[PARALLEL]
-部分的附加成本加速(预计利用更多(那些而且只有那些免费的)资源)。
永远是最难的部分War(还有待争取...).
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
-SCHEDULED fun()
的
of SCHEDULING & OCCUPIED RESOURCES' with hidden 1-ary RECURSION 对于任何规模不断扩大的 workload
很快就会变成 workload < someBound * 2 ^ W
,但代价是 W
(这导致 W * k
-多次重新{-获取,分配,发布} Wasted所有k
次请求#pragma omp task shared(...)
处理相关资源,在整个纯[SERIAL]
-by-definition递归潜水-&-重新浮出水面的过程中。
很容易看出有多少资源将挂起等待(由于甚至是 1 元 RECURSION 公式),直到每次潜入最深层次的递归气泡回到顶层 #pragma omp taskwait
.
为每个递归潜水级别重新分配新资源和新资源的成本通常会让您在开销严格的阿姆达尔定律(性能方面)上丧命,如果没有进入抖动或系统配置相关的溢出,由于较早地破坏了真实系统的物理资源,对于任何相当大的递归深度。
这些是您不需要支付的成本,如果不使用“典型便宜”但昂贵的(idle/wasted)资源递归问题公式,即使是最轻量级 1 元案例。
看看有多少:
-表示“等待线”除了在拓扑的任一阶段|
表示的“计算线”之外,还有多少并行的,waste/block,但必须让所有与任务相关的资源空闲(内存和堆栈-space 只是更可见的资源,它们在性能方面非常昂贵(只是让大部分处理时间空闲等待) 或如果超额订阅超出实际系统的配置容量,则容易因溢出而崩溃 )。
War就是你的了! 继续走...
站点合规免责声明:
---------------------------- ----------------------------------------------
根据 Whosebug 政策,完整的模型代码在此处 posted,对于任何情况,Godbolt.org platform might turn inaccessible, otherwise feel free to prefer and/or use the Compiler Explorer 工具都超出了顺序-chars 放入模型源代码中
选择和执行它的乐趣是永远你的: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)
}
}
}
}
下面我尝试绘制使用 OpenMP 任务并行化的代码。
在 main 函数中,一个并行环境被启动,之后代码立即被包装到 #pragma omp master
部分。在计算出预期的工作量并根据此工作量是否低于给定阈值后,必须完成的工作将传递给串行函数或递归地拆分工作量并初始化单独任务的函数。然后在 #pragma omp taskwait
指令之后汇总每个 #pragma omp task
的结果。
int main() {
#pragma omp parallel
{
#pragma omp master
{
//do some serial stuff
//estimate if parallelisation is worth it.
const int workload = estimateWorkload();
if (workload < someBound) {
serialFunction();
}
else {
parallelFunction(workload);
}
}
}
}
int parallelFunction(int workload) {
if (workload < someBound) {
return serialFunction();
}
int result1, result2;
#pragma omp task shared(result1)
{
result1 = parallelFunction(workload/2);
}
#pragma omp task shared(result2)
{
result2 = parallelFunction(workload/2);
}
#pragma omp taskwait
return result1 < result2;
}
在这样的设置下,如何衡量每个线程的实际计算时间?
如果我测量 CPU 时间并且有 k
个活动线程,那么我将得到 k*wallTime
,这是有道理的,因为线程是由前导 #pragma omp parallel
指令初始化并始终保持活动状态。然而,这并没有给我任何关于线程实际工作花费多少时间的信息,这使得代码难以分析。
Q : How do I measure the actual computing time of each thread in such a setting?
一个简单的 MOCK-UP CODE 用于简单的半手动代码执行时间分析:
不用说,对于“嘈杂”的执行平台,选择 CLOCK_MONOTONIC
可以节省漂移时间更新,但不会“节省”off-CPU-核心等待-状态,由于 O/S.
然而,对于原型设计阶段,这比安装所有“omp-native”callbacks'{ 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, ... }
处理要容易得多。
副作用奖励:
如果报告和 post 处理记录的嵌套代码执行各自的持续时间,那么简单的代码允许“构建”相关调用签名和递归嵌套相关开销的隐藏成本。
然后 revised, overhead-strict Amdahl's Law 不再撒谎并开始更准确地向您展示,当此代码开始丢失时 在与开销相关的(加上由于潜在的 工作单元的原子性(s) ) 原则上-[SERIAL]
-任何预期 True-[PARALLEL]
-部分的附加成本加速(预计利用更多(那些而且只有那些免费的)资源)。
永远是最难的部分War(还有待争取...).
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
-SCHEDULED fun()
的
of SCHEDULING & OCCUPIED RESOURCES' with hidden 1-ary RECURSION 对于任何规模不断扩大的 workload
很快就会变成 workload < someBound * 2 ^ W
,但代价是 W
(这导致 W * k
-多次重新{-获取,分配,发布} Wasted所有k
次请求#pragma omp task shared(...)
处理相关资源,在整个纯[SERIAL]
-by-definition递归潜水-&-重新浮出水面的过程中。
很容易看出有多少资源将挂起等待(由于甚至是 1 元 RECURSION 公式),直到每次潜入最深层次的递归气泡回到顶层 #pragma omp taskwait
.
为每个递归潜水级别重新分配新资源和新资源的成本通常会让您在开销严格的阿姆达尔定律(性能方面)上丧命,如果没有进入抖动或系统配置相关的溢出,由于较早地破坏了真实系统的物理资源,对于任何相当大的递归深度。
这些是您不需要支付的成本,如果不使用“典型便宜”但昂贵的(idle/wasted)资源递归问题公式,即使是最轻量级 1 元案例。
看看有多少:
-表示“等待线”除了在拓扑的任一阶段|
表示的“计算线”之外,还有多少并行的,waste/block,但必须让所有与任务相关的资源空闲(内存和堆栈-space 只是更可见的资源,它们在性能方面非常昂贵(只是让大部分处理时间空闲等待) 或如果超额订阅超出实际系统的配置容量,则容易因溢出而崩溃 )。
War就是你的了! 继续走...
站点合规免责声明:
---------------------------- ----------------------------------------------
根据 Whosebug 政策,完整的模型代码在此处 posted,对于任何情况,Godbolt.org platform might turn inaccessible, otherwise feel free to prefer and/or use the Compiler Explorer 工具都超出了顺序-chars 放入模型源代码中
选择和执行它的乐趣是永远你的: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)
}
}
}
}