为什么 Matlab 比 C++ 快 11 倍

Why is Matlab 11 times faster than C++

本文关键字:C++ Matlab 为什么      更新时间:2023-10-16

我正在比较 Matlab 和 C++ 之间香草看涨期权的蒙特卡罗定价算法的速度。这与为什么 MATLAB 在矩阵乘法中如此之快不同?由于加速不是由于矩阵乘法(只有一个快速完成的点积),而是由于其高效的高斯随机数生成器。

在 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中优化速度"。

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

这比 Matlab 慢 11 倍。

谁能解释一下区别?

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

就目前而言,您正在生成单线程代码。猜测,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 上,这在 ~.31 秒内运行。
在英特尔 i7 处理器上,这在 ~.16 秒内运行。

当然,如果你有一个拥有更多内核的CPU,那么你很有可能运行得更快。

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

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

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