为什么Matlab比C ++快11倍

时间:2016-03-21 12:10:36

标签: c++ matlab montecarlo matlab-compiler

我正在比较Matlab和C ++之间的Vanilla调用选项的蒙特卡罗定价算法的速度。这与Why is MATLAB so fast in matrix multiplication?不同,因为加速不是由于矩阵乘法(只有一个快速完成的点积),但似乎是由于其高效的高斯随机数发生器。

在Matlab中,代码已经过矢量化,代码如下所示

function [ value ] = OptionMCValue( yearsToExpiry, spot, strike, riskFreeRate, dividendYield, volatility, numPaths  )

    sd = volatility*sqrt(yearsToExpiry);
    sAdjusted = spot * exp( (riskFreeRate - dividendYield - 0.5*volatility*volatility) * yearsToExpiry);

    g = randn(1,numPaths);
    sT = sAdjusted * exp( g * sd );
    values = max(sT-strike,0);`
    value = mean(values);
    value = value * exp(-riskFreeRate * yearsToExpiry);

end

如果我使用以下1000万条路径运行

strike = 100.0;
yearsToExpiry = 2.16563;
spot = 100.0;
volatility = 0.20;
dividendYield = 0.03;
riskFreeRate = 0.05;
oneMillion = 1000000;
numPaths = 10*oneMillion;

tic
value = OptionMCValue( yearsToExpiry, spot, strike, riskFreeRate, dividendYield, volatility, numPaths  );
toc

我得到了

Elapsed time is 0.359304 seconds.
   12.8311

现在我在VS2013的C ++中做同样的事情

我的代码在OptionMC类中,如下所示

double OptionMC::value(double yearsToExpiry, 
                   double spot,
                   double riskFreeRate,
                   double dividendYield,
                   double volatility, 
                   unsigned long numPaths )
{
    double sd = volatility*sqrt(yearsToExpiry);
    double sAdjusted = spot * exp( (riskFreeRate - dividendYield - 0.5*volatility*volatility) * yearsToExpiry);
    double value = 0.0;
    double g, sT;

    for (unsigned long i = 0; i < numPaths; i++)
    {
        g = GaussianRVByBoxMuller();
        sT = sAdjusted * exp(g * sd);
        value += Max(sT - m_strike, 0.0);
    }

    value = value * exp(-riskFreeRate * yearsToExpiry);
    value /= (double) numPaths;
    return value;
}

BM代码如下

double GaussianRVByBoxMuller()
{
double result;
double x; double y;;
double w;

do
{
    x = 2.0*rand() / static_cast<double>(RAND_MAX)-1;
    y = 2.0*rand() / static_cast<double>(RAND_MAX)-1;
    w = x*x + y*y;
} while (w >= 1.0);

w = sqrt(-2.0 * log(w) / w);
result = x*w;

return result;
}

我已将优化选项设置为在Visual Studio中优化速度。

对于10米路径,需要4.124秒。

这比Matlab慢11倍。

任何人都可以解释这种差异吗?

编辑:在进一步测试时,减速似乎是对GaussianRVByBoxMuller的调用。 Matlab似乎有一个非常有效的实现 - Ziggurat方法。请注意,BM在这里是次优的,因为它生成2个RV而我只使用1.修复这个将使速度提高2倍。

1 个答案:

答案 0 :(得分:3)

现在,您正在生成单线程代码。猜测,Matlab正在使用多线程代码。这允许它以大约N的速度运行得更快,其中N = CPU中的核心数。

然而,这个故事还有更多的内容。还有一个问题是你正在使用rand(),它使用隐藏的全局状态。因此,如果您对代码进行简单的重写以使其成为多线程,那么rand()内部状态的冲突可能会阻止您提高速度(并且可能很容易)跑得慢 - 也许慢一点。)

为了解决这个问题,您可以考虑(例如)使用C ++ 11中添加的新随机数生成(以及可能的分发)类。通过这些,您可以为每个线程创建一个单独的随机数生成器实例,以防止内部状态发生冲突。

我重新编写了一些代码来使用它们,然后调用函数来获取它:

double m_strike = 100.0;

class generator {
    std::normal_distribution<double> dis;
    std::mt19937_64 gen;
public:
    generator(double lower = 0.0, double upper = 1.0)
        : gen(std::random_device()()), dis(lower, upper) {}

    double operator()() {
        return dis(gen);
    }
};

double value(double yearsToExpiry,
    double spot,
    double riskFreeRate,
    double dividendYield,
    double volatility,
    unsigned long numPaths)
{
    double sd = volatility*sqrt(yearsToExpiry);
    double sAdjusted = spot * exp((riskFreeRate - dividendYield - 0.5*volatility*volatility) * yearsToExpiry);
    double value = 0.0;
    double g, sT;

    generator gen;

// run iterations in parallel, with a private random number generator for each thread:
#pragma omp parallel for reduction(+:value) private(gen)
    for (long i = 0; i < numPaths; i++)
    {
        g = gen(); // GaussianRVByBoxMuller();
        sT = sAdjusted * exp(g * sd);
        value += std::max(sT - m_strike, 0.0);
    }

    value = value * exp(-riskFreeRate * yearsToExpiry);
    value /= (double)numPaths;
    return value;
}

int main() {
    std::cout << "value: " << value(2.16563, 100.0, 0.05, 0.03, 0.2, 10'000'000) << "\n";
}

我用VC ++ 2015编译了这个,使用以下命令行:

cl -openmp -Qpar -arch:AVX -O2b2 -GL test.cpp

在AMD A8-7600上运行时间约为0.32秒 在Intel i7处理器上,运行时间约为.16秒。

当然,如果你有一个拥有更多内核的CPU,你仍然有相当快的速度运行它。

就目前而言,我的代码需要VC ++ 2015而不是2013,但我怀疑它对性能的影响很大。它主要只是方便,比如使用10'000'000代替10000000(但我不打算在这台机器上安装2013的副本,只是为了弄明白我需要什么改变以适应它。)

另请注意,与最近的英特尔处理器相比,您可能(或可能不会)通过将arch:AVX更改为arch:AVX2来获得一些改进。

快速检查单线程代码表明您的Box-Muller分发代码可能比标准库的正常分发代码快一点,因此切换到线程友好版本可能会获得一点点速度更快(优化版 也应该大约加倍)。