在Matlab Mex文件中,TBB表现奇怪

5

编辑:< Matlab 限制 TBB 但不限制 OpenMP > 我的问题与上面那个不同,虽然使用相同的示例代码进行说明,但我在 tbb 初始化时指定了线程数,而没有使用“deferred”。另外,我谈论的是 TBB 在 c++ 和 TBB 在 mex 之间的奇怪行为。那个问题的答案只演示了在 C++ 中运行 TBB 时的线程初始化,而没有在 MEX 中。


我正在尝试提高 Matlab 的执行效率。当我在 mex 中使用 TBB 时,我遇到的奇怪现象是 TBB 初始化并没有像预期的那样工作。

这个 C++ 程序在单独执行时会有100% 的 CPU 占用率15 个 TBB 线程

main.cpp

#include "tbb/parallel_for_each.h"
#include "tbb/task_scheduler_init.h"
#include <iostream>
#include <vector>
#include "mex.h"

struct mytask {
  mytask(size_t n)
    :_n(n)
  {}
  void operator()() {
    for (long i=0;i<10000000000L;++i) {}  // Deliberately run slow
    std::cerr << "[" << _n << "]";
  }
  size_t _n;
};

template <typename T> struct invoker {
  void operator()(T& it) const {it();}
};

void mexFunction(/* int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[] */) {

  tbb::task_scheduler_init init(15);  // 15 threads

  std::vector<mytask> tasks;
  for (int i=0;i<10000;++i)
    tasks.push_back(mytask(i));

  tbb::parallel_for_each(tasks.begin(),tasks.end(),invoker<mytask>());

}

int main()
{
    mexFunction();
}

我稍微修改了一下代码,以便为Matlab创建一个MEX文件:

BuildMEX.mexw64

#include "tbb/parallel_for_each.h"
#include "tbb/task_scheduler_init.h"
#include <iostream>
#include <vector>
#include "mex.h"

struct mytask {
  mytask(size_t n)
    :_n(n)
  {}
  void operator()() {
    for (long i=0;i<10000000000L;++i) {}  // Deliberately run slow
    std::cerr << "[" << _n << "]";
  }
  size_t _n;
};

template <typename T> struct invoker {
  void operator()(T& it) const {it();}
};


void mexFunction( int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[] ) {

  tbb::task_scheduler_init init(15);  // 15 threads

  std::vector<mytask> tasks;
  for (int i=0;i<10000;++i)
    tasks.push_back(mytask(i));

  tbb::parallel_for_each(tasks.begin(),tasks.end(),invoker<mytask>());

}

请在Matlab中最终调用BuildMEX.mexw64。我将以下代码片段编译(mcc)为Matlab二进制文件“MEXtest.exe”,并使用vTune对其性能进行剖析(在MCR中运行)。该进程中的TBB仅初始化了4个tbb线程,而二进制文件仅占用了约50%的CPU。为什么MEX会降低整体性能和TBB? 我如何抓住更多cpu使用率来提高mex的性能?
function MEXtest()

BuildMEX();

end

我这里不是完全清楚,你是在使用MATLAB编译器(mcc)生成一个调用MEX函数的独立程序(因此在MCR上下文中运行)?还是将MEX函数与外部编译并且与MATLAB无关的普通C++程序进行比较?以下给出的答案似乎暗示了前者。 - Amro
以前我使用mcc生成了一个独立程序,该程序调用MEX函数并在MCR中运行。 - yfeng
2个回答

2
根据调度器类描述
这个类允许在一定程度上自定义TBB任务池的属性。例如,它可以限制由给定线程启动的并行工作的并发级别。它还可以用于指定TBB工作线程的堆栈大小,尽管如果线程池已经被创建,则此设置无效。
这在initialize()方法中进一步解释,该方法由构造函数调用:
如果存在任何其他task_scheduler_inits,则忽略number_of_threads。一个线程可以构造多个task_scheduler_inits。这样做不会有害,因为底层的调度器是引用计数的。
(我添加了突出部分)
我相信MATLAB已经在内部使用Intel TBB,并且必须在MEX函数执行之前在顶层初始化线程池。因此,您代码中的所有任务调度程序都将使用MATLAB内部部分指定的线程数,而忽略您在代码中指定的值。
默认情况下,MATLAB必须使用与物理处理器数量相等的线程池大小(而不是逻辑处理器),这表明在我的四核超线程机器上我得到:
>> maxNumCompThreads
Warning: maxNumCompThreads will be removed in a future release [...]
ans =
     4

另一方面,OpenMP没有调度程序,我们可以通过调用以下函数在运行时控制线程数量:

#include <omp.h>
.. 
omp_set_dynamic(1);
omp_set_num_threads(omp_get_num_procs());

或者通过设置环境变量:

>> setenv('OMP_NUM_THREADS', '8')

为了测试这个提出的解释,这里是我使用的代码:

test_tbb.cpp

#ifdef MATLAB_MEX_FILE
#include "mex.h"
#endif

#include <cstdlib>
#include <cstdio>
#include <vector>

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#include "tbb/task_scheduler_init.h"
#include "tbb/parallel_for_each.h"
#include "tbb/spin_mutex.h"

#include "tbb_helpers.hxx"

#define NTASKS 100
#define NLOOPS 400000L

tbb::spin_mutex print_mutex;

struct mytask {
    mytask(size_t n) :_n(n) {}
    void operator()()
    {
        // track maximum number of parallel workers run
        ConcurrencyProfiler prof;

        // burn some CPU cycles!
        double x = 1.0 / _n;
        for (long i=0; i<NLOOPS; ++i) {
            x = sin(x) * 10.0;
            while((double) rand() / RAND_MAX < 0.9);
        }
        {
            tbb::spin_mutex::scoped_lock s(print_mutex);
            fprintf(stderr, "%f\n", x);
        }
    }
    size_t _n;
};

template <typename T> struct invoker {
    void operator()(T& it) const { it(); }
};

void run()
{
    // use all 8 logical cores
    SetProcessAffinityMask(GetCurrentProcess(), 0xFF);

    printf("numTasks = %d\n", NTASKS);
    for (int t = tbb::task_scheduler_init::automatic;
         t <= 512; t = (t>0) ? t*2 : 1)
    {
        tbb::task_scheduler_init init(t);

        std::vector<mytask> tasks;
        for (int i=0; i<NTASKS; ++i) {
            tasks.push_back(mytask(i));
        }

        ConcurrencyProfiler::Reset();
        tbb::parallel_for_each(tasks.begin(), tasks.end(), invoker<mytask>());

        printf("pool_init(%d) -> %d worker threads\n", t,
            ConcurrencyProfiler::GetMaxNumThreads());
    }
}

#ifdef MATLAB_MEX_FILE
void mexFunction(int nlhs, mxArray* plhs[], int nrhs, const mxArray* prhs[])
{
    run();
}
#else
int main()
{
    run();
    return 0;
}
#endif

这是一个简单的辅助类代码,用于通过跟踪从线程池调用了多少个工作线程来分析并发性能。您始终可以使用Intel VTune或任何其他分析工具来获取相同类型的信息。

tbb_helpers.hxx

#ifndef HELPERS_H
#define HELPERS_H

#include "tbb/atomic.h"

class ConcurrencyProfiler
{
public:
    ConcurrencyProfiler();
    ~ConcurrencyProfiler();
    static void Reset();
    static size_t GetMaxNumThreads();
private:
    static void RecordMax();
    static tbb::atomic<size_t> cur_count;
    static tbb::atomic<size_t> max_count;
};

#endif

tbb_helpers.cxx

#include "tbb_helpers.hxx"

tbb::atomic<size_t> ConcurrencyProfiler::cur_count;
tbb::atomic<size_t> ConcurrencyProfiler::max_count;

ConcurrencyProfiler::ConcurrencyProfiler()
{
    ++cur_count;
    RecordMax();
}

ConcurrencyProfiler::~ConcurrencyProfiler()
{
    --cur_count;
}

void ConcurrencyProfiler::Reset()
{
    cur_count = max_count = 0;
}

size_t ConcurrencyProfiler::GetMaxNumThreads()
{
    return static_cast<size_t>(max_count);
}

// Performs: max_count = max(max_count,cur_count)
// http://www.threadingbuildingblocks.org/
//    docs/help/tbb_userguide/Design_Patterns/Compare_and_Swap_Loop.htm
void ConcurrencyProfiler::RecordMax()
{
    size_t o;
    do {
        o = max_count;
        if (o >= cur_count) break;
    } while(max_count.compare_and_swap(cur_count,o) != o);
}

首先,我将代码编译为本地可执行文件(我使用的是Intel C++ Composer XE 2013 SP1和VS2012 Update 4):

C:\> vcvarsall.bat amd64
C:\> iclvars.bat intel64 vs2012
C:\> icl /MD test_tbb.cpp tbb_helpers.cxx tbb.lib

我在系统Shell(Windows 8.1)中运行程序。它占用了100%的CPU利用率,我得到了以下输出:

C:\> test_tbb.exe 2> nul
numTasks = 100
pool_init(-1) -> 8 worker threads          // task_scheduler_init::automatic
pool_init(1) -> 1 worker threads
pool_init(2) -> 2 worker threads
pool_init(4) -> 4 worker threads
pool_init(8) -> 8 worker threads
pool_init(16) -> 16 worker threads
pool_init(32) -> 32 worker threads
pool_init(64) -> 64 worker threads
pool_init(128) -> 98 worker threads
pool_init(256) -> 100 worker threads
pool_init(512) -> 98 worker threads

正如我们所期望的那样,线程池被初始化成我们所要求的大小,并且被完全利用,受限于我们创建的任务数(在上一个案例中,我们为只有100个并行任务分配了512个线程!)。

接下来,我将代码编译为MEX文件:

>> mex -I"C:\Program Files (x86)\Intel\Composer XE\tbb\include" ...
   -largeArrayDims test_tbb.cpp tbb_helpers.cxx ...
   -L"C:\Program Files (x86)\Intel\Composer XE\tbb\lib\intel64\vc11" tbb.lib

当我在MATLAB中运行MEX函数时,这是我得到的输出:

>> test_tbb()
numTasks = 100
pool_init(-1) -> 4 worker threads
pool_init(1) -> 4 worker threads
pool_init(2) -> 4 worker threads
pool_init(4) -> 4 worker threads
pool_init(8) -> 4 worker threads
pool_init(16) -> 4 worker threads
pool_init(32) -> 4 worker threads
pool_init(64) -> 4 worker threads
pool_init(128) -> 4 worker threads
pool_init(256) -> 4 worker threads
pool_init(512) -> 4 worker threads

正如你所看到的,无论我们指定什么样的池大小,调度程序始终最多旋转4个线程来执行并行任务(4是我的四核机器上物理处理器的数量)。这证实了我在帖子开头所说的话。
请注意,我明确设置了处理器亲和力掩码以使用所有8个核心,但由于只有4个运行线程,在这种情况下CPU使用率大约保持在50%左右。
希望这有助于回答问题,并为长篇帖子感到抱歉 :)

这确实表明线程池在MEX函数有机会初始化之前被初始化和限制,并且与OP和我自己观察到的相符。答案是现有的任务调度程序一旦初始化就无法修改吗?肯定没有task_scheduler_init的方法允许进行此类更改... - chappjc
这也是我的结论;我认为一旦任务调度程序被初始化并且未终止(这是由MATLAB内部完成的),我们就无法更改线程池大小。 - Amro
@Amro 感谢您的演示,它证实了我的猜测。今天我开始在 MEX 中实现 OMP,并获得了预期的 8 个线程和 100% 的 CPU 使用率。但是,OMP 完成相同任务所需的时间要长得多,大部分工作都是调用 libiomp5md.dll。稍后我可能会在另一页上发布讨论此问题... - yfeng

1
假设您的计算机有超过4个物理核心,MATLAB独立进程的亲和掩码可能会限制可用的CPU。从实际的MATLAB安装调用的函数应该使用所有CPU,但对于使用MATLAB编译器生成的独立MATLAB应用程序可能不是这种情况。尝试再次运行测试,直接从MATLAB运行MEX函数。无论如何,您应该能够重置亲和掩码以使所有核心都可用于TBB,但我认为这种方法不能让TBB启动比您拥有的物理核心更多的线程。
背景:
自TBB 3.0更新4以来,处理器亲和设置是参考可用核心数的,根据开发者博客
所以,TBB唯一需要做的事情,而不是询问系统有多少个CPU,就是检索当前进程的关联掩码,计算其中非零位的数量,然后,TBB使用的工作线程不会超过必要的数量!这正是TBB 3.0更新4所做的。澄清我之前博客结尾陈述的话:TBB的方法tbb::task_scheduler_init::default_num_threads()tbb::tbb_thread::hardware_concurrency()返回的不仅仅是系统或当前处理器组中逻辑CPU的总数,而是根据其亲和性设置为进程提供的CPU数量
同样, tbb::default_num_threads文档指出了这一变化:
在TBB 3.0 U4之前,此方法返回系统中逻辑CPU的数量。目前,在Windows、Linux和FreeBSD上,它返回当前进程可用的逻辑CPU数量根据其关联掩码

tbb::task_scheduler_init::initialize的文档也建议线程数量“受处理器亲和性掩码限制”。

解决方法

要检查是否受亲和性掩码限制,可以使用Windows .NET函数:

numCoresInSystem = 16;
proc = System.Diagnostics.Process.GetCurrentProcess();
dec2bin(proc.ProcessorAffinity.ToInt32,numCoresInSystem)

输出字符串中任何位置都不应该有零,表示实际(存在于系统中)的核心。
您可以在MATLAB或C中设置亲和掩码,如Q&A中所述,Set processor affinity for MATLAB engine (Windows 7)。MATLAB方式:
proc = System.Diagnostics.Process.GetCurrentProcess();
proc.ProcessorAffinity = System.IntPtr(int32(2^numCoresInSystem-1));
proc.Refresh()

或者在 mexFunction 中使用 Windows API,在调用 task_scheduler_init 之前:
SetProcessAffinityMask(GetCurrentProcess(),(1 << N) - 1)

在*nix系统中,您可以调用taskset命令:
system(sprintf('taskset -p %d %d',2^N - 1,feature('getpid')))

谢谢您,chappjc,了解TBB的最新更新非常有用。不幸的是,我已经尝试了您提供的解决方案,无论是在matlab还是Window API方式下,都没有成功。我检查了我的计算机上的亲和掩码,它没有受到限制(笔记本电脑有8个逻辑核心,4个硬件核心,输出为8个)。然后我按照您的建议设置了亲和掩码,numCoresInSystem=8,在我的情况下,无论我尝试为TBB初始化多少个线程,TBB在MEX中始终初始化4个线程,并且MEXtest.exe利用了约50%的CPU。我还尝试直接在matlab中运行MEX函数,其性能大致相同。 - yfeng
@yfeng 真遗憾。我通过亲和掩码成功地减少了并行性,所以我认为你也面临着类似的问题。然而,我也只能达到50%的利用率,但那已经是所有物理核心(其他是超线程)。TBB似乎足够聪明,不会使用超过处理器亲和掩码的物理核心数量。我假设你实际上拥有多于4个计算核心...对吧? - chappjc
1
我在想MEX是否使用TBB并在我之前进行初始化,因此我的TBB初始化被忽略了。只是猜测。 - yfeng
我的目的是让MEX使用100%的CPU利用率。当我研究它时,TBB初始化是一个问题。 - yfeng
MATLAB和MCR都没有使用核心数量的限制(除非你自己调整过亲和性!),亲和掩码应默认使用所有处理器。我的解释(与您猜测的相同)是MATLAB初始化了TBB供其自己使用,并且在代码中更改线程池大小将被忽略...请参见我的答案。 - Amro
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接