TBB在Matlab Mex文件中表现得很奇怪

时间:2014-06-17 15:32:04

标签: multithreading matlab initialization mex tbb

编辑:其中Matlab limits TBB but not OpenMP> 我的问题与上面的问题不同,虽然使用相同的示例代码进行说明,但它并没有重复。在我的情况下,我在tbb初始化中指定了多个线程,而不是使用“deferred”。另外我在谈论cx中TBB与TBM中TBB之间的奇怪行为。该问题的答案仅演示了在C ++中运行TBB时的线程初始化,而不是在MEX中。


我正在尝试提升Matlab mex文件以提高性能。在mex中使用TBB时遇到的奇怪之处是TBB初始化不能按预期工作。

此C ++程序在执行 100%cpu 时使用 15 TBB线程 :< / p>

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?如何为mex获取更多cpu使用?

MEXtest.exe

function MEXtest()

BuildMEX();

end

2 个答案:

答案 0 :(得分:1)

假设您的计算机上有超过4个物理内核,MATLAB独立进程的关联掩码可能会限制可用的CPU。从实际MATLAB安装调用的函数应该使用所有CPU,但对于使用MATLAB编译器生成的独立MATLAB应用程序可能不是这种情况。再次尝试测试,直接从MATLAB运行MEX功能。在任何情况下,您都应该能够重置关联掩码以使TBB可以使用所有核心,但我认为这种方法不会让您强制TBB启动比您拥有物理核心更多的线程。

<强>背景

自TBB 3.0更新4以来,引用了处理器关联设置以确定可用内核的数量according to a developer blog

  

因此,TBB应该做的唯一事情就是检索当前的进程关联掩码,计算其中的非零位数,而不是向系统询问它有多少CPU,而且,TBB不再使用工作者线程超过必要!这正是TBB 3.0 Update 4所做的。澄清我之前博客结尾处的陈述 TBB的方法tbb::task_scheduler_init::default_num_threads()tbb::tbb_thread::hardware_concurrency()不仅返回系统或当前处理器组中逻辑CPU的总数,而且还返回CPU的数量可根据其亲和力设置进行处理。

同样,the docs for tbb::default_num_threads表示此更改:

  

在TBB 3.0 U4之前,此方法返回系统中逻辑CPU的数量。目前在Windows,Linux和FreeBSD上,它根据其关联掩码返回当前进程可用的逻辑CPU数。

The docs for tbb::task_scheduler_init::initialize还表明线程数“受处理器关联掩码限制”。

解决

要检查您是否受到关联掩码的限制,可以使用Windows .NET功能:

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

输出字符串在任何表示真实(存在于系统中)核心的位置都不应该为零。

您可以在MATLAB或C中设置关联掩码,如Q&amp; 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')))

答案 1 :(得分:1)

根据scheduler class description

  

此类允许为某些人定制TBB任务池的属性   程度。例如,它可以限制并行工作的并发级别   由给定的线程发起。它也可以用于指定堆栈   TBB工作线程的大小,尽管此设置无效   如果已经创建了线程池

在构造函数调用的initialize() methods中进一步解释了这一点:

  

如果当前存在任何其他task_scheduler_inits,则忽略number_of_threads 。线程可以构造多个   task_scheduler_inits。这样做不会有害,因为潜在的   调度程序是引用计数。

(由我添加的突出显示的部分)

我相信MATLAB已在内部使用英特尔TBB,它必须在执行MEX功能之前在顶层初始化一个线程池。因此,代码中的所有任务调度程序将使用MATLAB内部部分指定的线程数,忽略您在代码中指定的值。

默认情况下,MATLAB必须初始化大小为equal to the number of physical processors(不是逻辑)的线程池,这可以通过我的四核超线程机器得到的事实表明:

>> 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);
}

首先我将代码编译为本机可执行文件(我使用的是英特尔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)中运行该程序。它的CPU利用率达到100%,我得到以下输出:

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%。

希望这有助于回答这个问题,对于长篇文章感到抱歉:)