【问题标题】:Math.js is slow to multiply 2 big matricesMath.js 乘以 2 个大矩阵很慢
【发布时间】:2019-01-23 13:22:44
【问题描述】:

我正在尝试加速纯 JavaScript 中的矩阵乘法。几百行以上的乘法似乎很慢,一千行以上的一分钟:您将在下面看到执行时间。

你会如何解决这个问题?我们正在开发 Node.js 中的拆分 + 并行化解决方案,因此我正在寻找在纯 JavaScript 中优化它的最佳选项。我的解决方案必须使并行化流本身适应可用的 CPU 线程数(这在设计时是未知的)。

一些数据:

const math = require("mathjs");

// a1 is a 1000x1000 float matrix
// b1 is a 1000x400
math.multiply(a1, b1)
// runs in 19.6 seconds on a CPU 4.2Ghz

// a2 is 1600x1200
// b2 is 1200x800
math.multiply(a2, b2)
// runs in 78 seconds

【问题讨论】:

    标签: javascript node.js performance math matrix-multiplication


    【解决方案1】:

    数组查找优化

    数组是 JavaScipt 中的关联查找表——它们本质上是低效的。这种数组访问的优化

    var array = some_2D_Array;
    var nRows = array.length;
    var nCols = array[0].length;
    
    for( var r = 0; r < nRows; ++r) {
        for( var c = 0; c < nCols; ++c) {
            // do something with array[r][c];
        }
    }
    

    是替换成

    var array = some_2D_Array;
    var nRows = array.length;
    var nCols = array[0].length;
    
    for( var r = 0; r < nRows; ++r) {
        var aRow = array[r];  // lookup the row once
        for( var c = 0; c < nCols; ++c) {
            // do something with aRow[c];
        }
    }
    

    这避免了在内部循环的每次迭代中搜索 array 对象的行数组。性能提升取决于 JS 引擎和内部迭代次数。

    类型化数组用法

    另一种选择是使用一维类型化数组来避免关联数组索引查找而不是计算它。这是我在 Node 中运行的一些测试代码,看看它可能会产生什么不同:

    function Mat (rows, cols) {
        var length = rows*cols,
            buffer = new Float64Array( length)
        ;
        function getRow( r) {
            var start = r*cols,
                inc = 1;
            return { length: cols, start, inc, buffer};
        }
        function getCol( c) {
            var start = c,
                inc = cols;
            return { length: rows, start, inc, buffer};
        }
        function setRC(r,c, to) {
            buffer[ r*cols + c] = to;
        }
        this.rows = rows;
        this.cols = cols;
        this.buffer = buffer;
        this.getRow = getRow;
        this.getCol = getCol;
        this.setRC = setRC;
    
    }
    Mat.dotProduct = function( vecA, vecB) {
        var acc=0,
            length = vecA.length,
            a = vecA.start, aInc = vecA.inc, aBuf = vecA.buffer,
            b = vecB.start, bInc = vecB.inc, bBuf = vecB.buffer
        ;
        if( length != vecB.length) {
            throw "dot product vectors of different length";
        }
        while( length--) {
            acc += aBuf[ a] * bBuf[ b];
            a += aInc;
            b += bInc;
        }
        return acc;
    }
    Mat.mul = function( A, B, C) {
        if( A.cols != B.rows) {
            throw "A cols != B.rows";
        }
        if( !C) {
            C = new Mat( A.rows, B.cols);
        }
        for( var r = 0; r < C.rows; ++r) {
            var Arow = A.getRow(r);
            for (var c = 0; c < C.cols; ++c) {
                C.setRC( r, c, this.dotProduct( Arow, B.getCol(c)));
            }
        }
        return C;
    }
    
    function test() {
        // A.cols == B.rows
        let side = 128;
        let A = new Mat( side, side)
        let B= new Mat( side, side);
        A.buffer.fill(1)
        B.buffer.fill(1)
    	console.log( "starting test");
        let t0 = Date.now();
        Mat.mul( A,B);
        let t1 = Date.now();
    
        console.log( "time: " + ((t1-t0)/1000).toFixed(2) +" seconds");
    }
    test()

    两个方阵相乘的结果(1.1Ghz Celeron):

    // 128 x 128 = 0.05 秒
    // 256 x 256 = 0.14 秒
    // 512 x 512 = 7 秒
    // 768 x 768 = 25 秒
    // 1024 x 1024 = 58 秒
    

    CPU 速度的差异表明这种方法可能会明显更快但是 ...代码是实验性的,系统没有其他负载并且时序用于数组乘法单独 - 它们排除了解码和用数据填充数组所花费的时间。任何重大收获都需要在实践中得到证明。

    我最终决定,当将两个方阵相乘时,将使用的边尺寸加倍应该使运算花费 8 倍的时间:计算结果元素的数量是计算点积的四倍,用于计算点积的向量中的元素数量是两倍。 512 x 512 和 1024 x 1024 乘法的比较时间符合这一预期。

    【讨论】:

    • 是的,我在第二个循环之前设置了这个变量。但是你会完全重做算法还是仍然使用 math.js 库?我们有一个解决方案的开始,根据 CPU 的数量和 A 矩阵大小,B 矩阵大小,将各种大小的矩阵按带宽或分块分割......然后我们仍然使用 Math.js 来计算子矩阵.这是非常实验性的......
    • 感谢您非常详细的回答!我将对此进行详细测试。为了减少执行时间,我目前正在探索将代码执行与子进程或工作线程并行化的可能性。最后一个选项听起来更准确和最新。我会尽快发布我的代码,使其正常工作。在计算任何东西之前,与其他线程的通信成本似乎很高。有人曾经尝试过使用工作线程进行如此细粒度的并行化吗?
    • 仅供参考,刚刚找到一个共享内存包,但自己从未使用过:npmjs.com/package/mmap-object
    • 哦,谢谢!我正在对不同的解决方案进行基准测试,以将大型数据集从主线程传递给工作线程,事实上,到目前为止,文件是最好的选择。谢谢你的这个库。我会在完成后立即发布我的测试结果
    猜你喜欢
    • 2023-03-12
    • 2013-12-20
    • 1970-01-01
    • 2015-01-28
    • 2012-11-13
    • 2020-01-24
    • 2021-07-01
    • 2021-04-24
    • 2021-01-19
    相关资源
    最近更新 更多