有许多方法可以在 JS 中表明矩阵数学。有些方法可读性强,有些方法速度快。我想探索一下这些差异。某些技术实际上能为我节省多少时间?
为此,我将只研究一个操作:逐元素加法以减少总案例数,但差异操作可能会稍微改变整体值,尤其是像矩阵乘法这样需要稍微复杂一些的应用程序规则的运算。这些状态也在我的计算机上,它是稍旧的 i7 8700K,使用 Deno,其底层是 v8。如果有不同的优化,像 Bun 这样的不同运行时可能会表现得超级不同。
本文相关的代码可以从这里下载。

NSDT工具推荐: Three.js AI纹理开发包 – YOLO合成数据生成器 – GLTF/GLB在线编辑 – 3D模型格式在线转换 – 可编程3D场景编辑器 – REVIT导出3D模型插件 – 3D模型语义搜索引擎 – Three.js虚拟轴心开发包 – 3D模型在线减面 – STL模型在线切割
1、简单的函数式方法
我想从这里开始,由于这是我为初稿编写它的方式。它的代码超级优化,但我怀疑性能实际上相当糟糕。不变性对于避免错误超级有用,但对于性能却很糟糕,尤其是当 JS 没有智能副本时。
//mat.js
export function addMatrixFunc(a, b) {
return a.map((row, ri) => row.map((val, ci) => b[ri][ci] + val));
}
矩阵表明是数组的数组。外部数组是行,内部数组是列。
使用 Deno 的内置基准测试工具,我们可以看到它在不同大小矩阵上的表现。
import { addMatrixFunc } from "./mat.js";
import { mat100A, mat100B } from "./data/mat-data.js";
Deno.bench("Add 1x1", () => {
addMatrixFunc([[44]], [[65]]);
});
Deno.bench("Add 2x2", () => {
addMatrixFunc([[44]], [[65]]);
});
Deno.bench("Add 4x4", () => {
addMatrixFunc([[44]], [[65]]);
});
/* ... */
Deno.bench("Add 100x100", () => {
addMatrixFunc(mat100A, mat100B);
});
mat100A 和 mat100B 是预先生成的 100×100 矩阵,太大了,无法放入测试文件中。
需要注意的是,我认为 Deno 至少不再允许您设置迭代或预热迭代。我认为它只是寻找数字的收敛。实际运行次数显示在 JSON 输出中,并且每个测试略有不同。
以下是我们的操作方式:
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (Func) |
63ns |
180ns |
70ns |
74ns |
113ns |
124ns |
|
Add 2×2 (Func) |
144ns |
208ns |
152ns |
158ns |
184ns |
196ns |
|
Add 4×4 (Func) |
312ns |
373ns |
329ns |
335ns |
370ns |
373ns |
|
Add 8×8 (Func) |
694ns |
930ns |
724ns |
731ns |
930ns |
930ns |
|
Add 16×16 (Func) |
1798ns |
1942ns |
1836ns |
1843ns |
1942ns |
1942ns |
|
Add 32×32 (Func) |
5274ns |
6599ns |
5495ns |
5605ns |
6599ns |
6599ns |
|
Add 64×64 (Func) |
13000ns |
2331200ns |
17451ns |
16300ns |
41900ns |
60700ns |
|
Add 100×100 (Func) |
30800ns |
512800ns |
40269ns |
38200ns |
105700ns |
218300ns |
2、循环
所以我认为我们可以改善的第一个方法是循环。函数有开销,所以如果我们去掉它,并且更具有命令性,我们就可以更快一些。
export function addMatrixLoop(a, b) {
const out = [];
for (let row = 0; row < a.length; row++) {
const arrayRow = [];
for (let col = 0; col < a[0].length; col++) {
arrayRow.push(a[row][col] + b[row][col])
}
out.push(arrayRow);
}
return out;
}
请注意,我不会进行严格的边界检查,我们只是假设 a 和 b 的大小一样,由于边界检查只会增加开销。
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (Loop) |
28ns |
210ns |
46ns |
47ns |
142ns |
168ns |
|
Add 2×2 (Loop) |
55ns |
163ns |
71ns |
76ns |
125ns |
143ns |
|
Add 4×4 (Loop) |
122ns |
227ns |
143ns |
151ns |
195ns |
225ns |
|
Add 8×8 (Loop) |
360ns |
807ns |
411ns |
422ns |
744ns |
807ns |
|
Add 16×16 (Loop) |
1179ns |
1246ns |
1208ns |
1217ns |
1246ns |
1246ns |
|
Add 32×32 (Loop) |
5031ns |
5216ns |
5090ns |
5105ns |
5216ns |
5216ns |
|
Add 64×64 (Loop) |
14300ns |
362400ns |
20651ns |
19200ns |
52900ns |
110500ns |
|
Add 100×100 (Loop) |
38200ns |
425400ns |
54401ns |
54100ns |
227700ns |
256300ns |
循环开始时速度更快,但一旦达到 32×32 左右,它们就等于 .map,并且大于 .map 时速度更快。超级令人惊讶!
3、预分配数组
我的下一个想法是预分配数组,由于推入数组可能会导致重新调整大小,也许这就是速度较慢的缘由。
export function addMatrixLoopPreAlloc(a, b) {
const out = new Array(a.length);
for (let row = 0; row < a.length; row++) {
const arrayRow = new Array(a[0].length);
for (let col = 0; col < a[0].length; col++) {
arrayRow[col] = a[row][col] + b[row][col];
}
out[row] = arrayRow;
}
return out;
}
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (Loop Prealloc) |
13ns |
137ns |
18ns |
20ns |
56ns |
73ns |
|
Add 2×2 (Loop Prealloc) |
25ns |
65ns |
28ns |
27ns |
45ns |
53ns |
|
Add 4×4 (Loop Prealloc) |
61ns |
152ns |
73ns |
78ns |
124ns |
129ns |
|
Add 8×8 (Loop Prealloc) |
203ns |
444ns |
228ns |
232ns |
348ns |
434ns |
|
Add 16×16 (Loop Prealloc) |
710ns |
942ns |
762ns |
768ns |
942ns |
942ns |
|
Add 32×32 (Loop Prealloc) |
2648ns |
2769ns |
2700ns |
2716ns |
2769ns |
2769ns |
|
Add 64×64 (Loop Prealloc) |
9500ns |
372100ns |
10926ns |
10100ns |
25000ns |
35800ns |
|
Add 100×100 (Loop Prealloc) |
24500ns |
515800ns |
28392ns |
26300ns |
62100ns |
204400ns |
成功了!我们比开始时快了 1.5 倍!
4、展开循环
如果我们删除所有循环并用手写出来会怎么样?
export function addMatrix4x4(a, b) {
return [
[a[0][0] + b[0][0], a[0][1] + b[0][1], a[0][2] + b[0][2], a[0][3] + b[0][3]],
[a[1][0] + b[1][0], a[1][1] + b[1][1], a[1][2] + b[1][2], a[1][3] + b[1][3]],
[a[2][0] + b[2][0], a[2][1] + b[2][1], a[2][2] + b[2][2], a[2][3] + b[2][3]],
[a[3][0] + b[3][0], a[3][1] + b[3][1], a[3][2] + b[3][2], a[3][3] + b[3][3]]
];
}
这不太灵活,由于需要为要添加的每种矩阵形状都提供一个函数。但是,在某些情况下(例如 3D),情况还不算太糟,由于我们拥有的东西数量超级有限,一般只有 4×4。在机器学习中,这可能会导致问题。
这是一个为展开循环生成 javascript 文本的函数:
export function genMatAddBody(rows, cols) {
let funcBody = "return [
";
for (let r = 0; r < rows; r++) {
funcBody += " ["
for (let c = 0; c < cols; c++) {
funcBody += `a[${r}][${c}] + b[${r}][${c}]${c < cols - 1 ? ", " : ""}`
}
funcBody += `]${r < rows - 1 ? ", " : ""}
`
}
funcBody += ` ];
`
return funcBody;
}
export function genMatAddFunc(rows, cols) {
rows = Number(rows);
cols = Number(cols);
const body = genMatAddBody(rows, cols);
return new Function("a", "b", body);
}
我也很好奇这种动态生成是否会带来很大的变化:
export function genMatAddFunc(rows, cols) {
rows = Number(rows); //prevents code injection
cols = Number(cols);
const body = genMatAddBody(rows, cols);
return new Function("a", "b", body);
}
由于我们使用 eval,所以我们应该确保清理输入:
const addMatrix1x1Dyn = genMatAddFunc(1,1);
const addMatrix2x2Dyn = genMatAddFunc(2,2);
const addMatrix4x4Dyn = genMatAddFunc(4,4);
// etc.
const addMatrix100x100Dyn = genMatAddFunc(100,100);
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (unrolled) |
7ns |
34ns |
8ns |
8ns |
19ns |
20ns |
|
Add 1×1 (unrolled dynamic) |
7ns |
40ns |
8ns |
7ns |
19ns |
20ns |
|
Add 2×2 (unrolled) |
11ns |
46ns |
13ns |
12ns |
26ns |
29ns |
|
Add 2×2 (unrolled dynamic) |
11ns |
39ns |
12ns |
12ns |
27ns |
29ns |
|
Add 4×4 (unrolled) |
36ns |
159ns |
59ns |
72ns |
124ns |
130ns |
|
Add 4×4 (unrolled dynamic) |
36ns |
236ns |
67ns |
84ns |
156ns |
181ns |
|
Add 8×8 (unrolled) |
92ns |
243ns |
130ns |
142ns |
235ns |
242ns |
|
Add 8×8 (unrolled dynamic) |
89ns |
262ns |
113ns |
119ns |
186ns |
209ns |
|
Add 16×16 (unrolled) |
500ns |
672800ns |
734ns |
600ns |
3400ns |
10500ns |
|
Add 16×16 (unrolled dynamic) |
500ns |
2052000ns |
799ns |
600ns |
6400ns |
10600ns |
|
Add 32×32 (unrolled) |
73800ns |
562500ns |
83976ns |
85200ns |
136400ns |
160600ns |
|
Add 32×32 (unrolled dynamic) |
73000ns |
908200ns |
90772ns |
90900ns |
137900ns |
162600ns |
|
Add 64×64 (unrolled) |
328700ns |
737300ns |
350104ns |
343900ns |
574500ns |
587000ns |
|
Add 64×64 (unrolled dynamic) |
327600ns |
698800ns |
349201ns |
345400ns |
573900ns |
592400ns |
|
Add 100×100 (unrolled) |
829600ns |
1250900ns |
876580ns |
873700ns |
1143900ns |
1157500ns |
|
Add 100×100 (unrolled dynamic) |
816900ns |
1416300ns |
891844ns |
894500ns |
1227700ns |
1288200ns |
对于较小的值,这是一个很大的改善,比预分配的循环快了大约 1.5 到 2 倍,但对于较大的值,速度要慢得多,这可不是件好事。我不确定为什么会这样,也许这与函数本身的大小有关?生成的代码超级庞大。此外,动态生成与写出它们基本一样。因此,如果想节省有效载荷(并且不受 CSP 的限制),你可以动态创建它们而不会受到任何惩罚。
5、展平数组
我认为我们可以节省的另一件事是数组。从技术上讲,我们不需要有许多嵌套数组,它们会增加一些创建开销。所以目前一个 2×2 数组看起来像这样:
[
4, 7,
10, 5
]
但是目前你需要知道尺寸才能实现这个功能,由于不同的矩形可以有一样数量的元素。所以也许我们可以把它变成一个对象。
{
shape: [2,2],
data: [
4, 7,
10, 5
]
}
形状是一个数组而不是属性,由于我们可以将这个想法扩展到 N 维张量。实际上,这就是 tensorflowjs 等库所做的。为了方便起见,让我们构建一些函数来在格式之间进行转换。
export function nestedArrayToFlat(nested){
return {
shape: [nested.length, nested[0].length],
data: nested.flat(Infinity)
}
}
export function flatToNestedArray(flat){
const data = new Array(flat.shape[0]);
for(let row = 0; row < flat.shape[0]; row++){
const rowArray = new Array(flat.shape[1]);
for(let col = 0; col < flat.shape[1]; col++){
rowArray[col] = flat.data[row * flat.shape[1] + col];
}
data[row] = rowArray;
}
return data;
}
到目前为止,我认为预分配数组和循环在扩展到较大值时具有最佳的总体性能,因此我们暂时坚持这一点。这也意味着我将省略平面和循环,由于它们在任何类别中都没有获胜,以及动态,由于它与展开一样。
export function addMatrixFlat(a, b) {
const out = {
shape: a.shape,
data: new Array(a.data.length)
};
for (let row = 0; row < a.shape[0]; row++) {
for (let col = 0; col < a.shape[1]; col++) {
const index = (row * a.shape[1]) + col;
out.data[index] = a.data[index] + b.data[index];
}
}
return out;
}
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (flat) |
9ns |
53ns |
10ns |
10ns |
24ns |
29ns |
|
Add 2×2 (flat) |
14ns |
49ns |
15ns |
15ns |
29ns |
30ns |
|
Add 4×4 (flat) |
32ns |
107ns |
40ns |
46ns |
86ns |
94ns |
|
Add 8×8 (flat) |
97ns |
167ns |
110ns |
113ns |
143ns |
157ns |
|
Add 16×16 (flat) |
400ns |
548ns |
436ns |
447ns |
517ns |
548ns |
|
Add 32×32 (flat) |
1985ns |
2900ns |
2222ns |
2276ns |
2900ns |
2900ns |
|
Add 64×64 (flat) |
8512ns |
10514ns |
8775ns |
8715ns |
10514ns |
10514ns |
|
Add 100×100 (flat) |
15500ns |
701100ns |
23261ns |
21800ns |
54200ns |
194800ns |
在处理较大的矩阵时,它比我们之前的最佳结果快了约 20%,但在处理 1×1 和 2×2 时,它比展开时慢了 20%。由于这些并不是太重大,我认为这是一个巨大的胜利。
6、行优先还是列优先
如果我们遍历行还是列,这有关系吗?有人可能会怀疑,当涉及到 CPU 缓存时,这可能会有关系,但让我们测试一下。
export function addMatrixFlatColMajor(a, b) {
const out = {
shape: a.shape,
data: new Array(a.data.length)
};
for (let col = 0; col < a.shape[1]; col++) {
for (let row = 0; row < a.shape[0]; row++) {
const index = (row * a.shape[1]) + col;
out.data[index] = a.data[index] + b.data[index];
}
}
return out;
}
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (flat col major) |
9ns |
41ns |
10ns |
9ns |
21ns |
22ns |
|
Add 2×2 (flat col major) |
14ns |
41ns |
15ns |
14ns |
29ns |
32ns |
|
Add 4×4 (flat col major) |
32ns |
79ns |
37ns |
37ns |
61ns |
67ns |
|
Add 8×8 (flat col major) |
101ns |
156ns |
114ns |
116ns |
147ns |
153ns |
|
Add 16×16 (flat col major) |
423ns |
532ns |
453ns |
465ns |
513ns |
532ns |
|
Add 32×32 (flat col major) |
2047ns |
3228ns |
2199ns |
2258ns |
3228ns |
3228ns |
|
Add 64×64 (flat col major) |
7500ns |
413800ns |
10417ns |
10200ns |
26200ns |
37000ns |
|
Add 100×100 (flat col major) |
19800ns |
575300ns |
25090ns |
23500ns |
63000ns |
198500ns |
实际证明,列主遍历实际上比行主遍历慢一点。这可能是由于缓存行的读取方式更优化。
但是,由于逐元素添加超级简单,我们实际上可以放弃循环结构,只需使用单个循环线性添加所有元素即可。
export function addMatrixFlatSimple(a, b) {
const out = {
shape: a.shape,
data: new Array(a.data.length)
};
for(let i = 0; i < a.data.length; i++){
out.data[i] = a.data[i] + b.data[i];
}
return out;
}
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (flat simple) |
7ns |
46ns |
8ns |
8ns |
18ns |
20ns |
|
Add 2×2 (flat simple) |
9ns |
54ns |
10ns |
10ns |
23ns |
26ns |
|
Add 4×4 (flat simple) |
18ns |
77ns |
24ns |
28ns |
51ns |
56ns |
|
Add 8×8 (flat simple) |
55ns |
159ns |
73ns |
78ns |
125ns |
136ns |
|
Add 16×16 (flat simple) |
276ns |
405ns |
315ns |
335ns |
393ns |
405ns |
|
Add 32×32 (flat simple) |
1387ns |
1682ns |
1490ns |
1547ns |
1682ns |
1682ns |
|
Add 64×64 (flat simple) |
6381ns |
7219ns |
6602ns |
6675ns |
7219ns |
7219ns |
|
Add 100×100 (flat simple) |
9000ns |
598000ns |
17166ns |
15700ns |
49400ns |
178400ns |
这比原先快了 20% 以上。
7、展开
我们也可以展开这些,看看会发生什么,也许更简单的结构会有所协助?使用此代码:
export function genMatAddFlatBody(rows, cols){
let funcBody = "return [
";
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
funcBody += `a[${r * cols + c}] + b[${r * cols + c}]${(c * r) < ((rows - 1) * (cols - 1)) ? ", " : ""}`
}
}
funcBody += `];
`
return funcBody;
}
我们可以生成如下函数:
export function addMatrixFlat2x2(a, b) {
return [
a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
}
我们可以使用 eval 像这样动态创建它们:
export function genMatAddFlatFunc(rows, cols) {
rows = Number(rows);
cols = Number(cols);
const body = genMatAddFlatBody(rows, cols);
return new Function("a", "b", body);
}
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (flat unrolled) |
6ns |
53ns |
7ns |
7ns |
19ns |
22ns |
|
Add 2×2 (flat unrolled) |
7ns |
62ns |
8ns |
8ns |
21ns |
23ns |
|
Add 4×4 (flat unrolled) |
24ns |
136ns |
37ns |
41ns |
84ns |
93ns |
|
Add 8×8 (flat unrolled) |
61ns |
185ns |
81ns |
86ns |
131ns |
144ns |
|
Add 16×16 (flat unrolled) |
300ns |
564700ns |
508ns |
400ns |
1000ns |
6100ns |
|
Add 32×32 (flat unrolled) |
63600ns |
826700ns |
74574ns |
75200ns |
133000ns |
162600ns |
|
Add 64×64 (flat unrolled) |
263500ns |
788800ns |
286503ns |
280600ns |
502900ns |
528900ns |
|
Add 100×100 (flat unrolled) |
706400ns |
1760300ns |
764369ns |
758900ns |
1102800ns |
1118900ns |
它只是在 1×1 和 2×2 时超越了简单循环,而在此之后,它会在较大尺寸时丢失并变得更糟。
8、类型化数组
因此,我能看到下一个可能的优化领域是实际使用类型。我们可以在 Javascript 中使用类型化数组来实现这一点。这将使我们能够分配一个内存块并减少任何数组结构的开销。但这实际上更重大一些。
通过使用类型化数组,我们实际上可以减少转换。WASM、WebGL 和 WebGPU 等 API 处理内存块,我们需要转换的越少,我们结束的速度就越快。所以我认为即使结果有点慢,依旧有充分的理由去追求它。
虽然我们最终得到了不同的路径,一个用于浮点数,一个用于整数,即使如此,如果我们选择不同的位宽,也可能很重大。
此外,由于我们已经表明平面结构总体上表现更好,所以我们不需要思考嵌套数组。
为了简洁起见,我不会测试所有类型数组组合,由于我们将开始看到一个一般模式。
Float 64
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (F64) |
330ns |
1600ns |
400ns |
397ns |
663ns |
1600ns |
|
Add 2×2 (F64) |
329ns |
598ns |
393ns |
409ns |
493ns |
598ns |
|
Add 4×4 (F64) |
393ns |
1786ns |
490ns |
503ns |
662ns |
1786ns |
|
Add 8×8 (F64) |
490ns |
778ns |
621ns |
664ns |
778ns |
778ns |
|
Add 16×16 (F64) |
1024ns |
5425ns |
1311ns |
1334ns |
5425ns |
5425ns |
|
Add 32×32 (F64) |
3346ns |
4707ns |
3772ns |
4115ns |
4707ns |
4707ns |
|
Add 64×64 (F64) |
8000ns |
2309700ns |
14203ns |
12700ns |
35300ns |
44800ns |
|
Add 100×100 (F64) |
23200ns |
3328400ns |
35026ns |
33300ns |
82400ns |
231000ns |
JavaScript 数字是 64 位浮点数。因此,它们的表现比普通的 JavaScript 数组慢的确 令人惊讶。处理小数组实际上比 array.map 慢。我猜这与引擎处理它们的方式有关。随着矩阵变大,它们会变得更快,但即使在 100×100 个项目时,它依旧比普通平面数组慢许多。
Float 32
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (F32) |
324ns |
554ns |
380ns |
391ns |
506ns |
554ns |
|
Add 2×2 (F32) |
324ns |
594ns |
391ns |
408ns |
520ns |
594ns |
|
Add 4×4 (F32) |
396ns |
658ns |
463ns |
489ns |
569ns |
658ns |
|
Add 8×8 (F32) |
508ns |
822ns |
620ns |
673ns |
822ns |
822ns |
|
Add 16×16 (F32) |
1148ns |
1784ns |
1345ns |
1422ns |
1784ns |
1784ns |
|
Add 32×32 (F32) |
3258ns |
3840ns |
3344ns |
3337ns |
3840ns |
3840ns |
|
Add 64×64 (F32) |
10500ns |
1101800ns |
18473ns |
21600ns |
66500ns |
101200ns |
|
Add 100×100 (F32) |
25800ns |
1797500ns |
37062ns |
35800ns |
99800ns |
245400ns |
F32 数组与 Float64 数组存在同样的问题。尽管更小,但性能几乎一样,因此单纯从速度角度思考,选择它们毫无意义。实际上,在 100×100 时,F64 数组的速度相当快。我们获得的唯一好处是内存减少一半,这可能是选择它们的一个缘由。
Int 32
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (I32) |
321ns |
1015ns |
390ns |
398ns |
704ns |
1015ns |
|
Add 2×2 (I32) |
324ns |
570ns |
390ns |
403ns |
501ns |
570ns |
|
Add 4×4 (I32) |
372ns |
530ns |
426ns |
443ns |
488ns |
530ns |
|
Add 8×8 (I32) |
455ns |
621ns |
539ns |
575ns |
616ns |
621ns |
|
Add 16×16 (I32) |
784ns |
1202ns |
913ns |
966ns |
1202ns |
1202ns |
|
Add 32×32 (I32) |
2111ns |
2704ns |
2182ns |
2182ns |
2704ns |
2704ns |
|
Add 64×64 (I32) |
8742ns |
9569ns |
9138ns |
9305ns |
9569ns |
9569ns |
|
Add 100×100 (I32) |
12600ns |
2578300ns |
22470ns |
21600ns |
50300ns |
72200ns |
I32 再次表现出类似的行为,但在较大的矩阵中开始看到更大的收益。实际上,在 100×100 时,I32 矩阵大约等于平面矩阵。这并不令人惊讶,但如果你正在处理大型整数矩阵,这可能是你的最佳选择。
9、结束语
对于简单的单线程 javascript,我们观察到了一些事情(在 Deno/V8 @ 2023-03-31):
- 循环的性能大多优于 .map,除非值超级大并且只使用嵌套数组(我尝试了一个平面数组,但它不足以复制粘贴数据)。
- 定制的展开函数在 4×4 或更小的超级小的尺寸上效果很好,但无法击败简单的循环并且会超级超级快地下降。
- 减少结构会带来很大的不同。
- 预分配数组会带来巨大的不同,如果可以的话,请始终这样做。
- 类型化数组没有速度优势(但我们可能会获得更少的转换开销和空间节省)。
我们可以用更多方式来处理矩阵,我可能想看看 WASM 和 WebGPU 是什么样子的,它们开销很大,但由于并行性,实际计算速度可能会大幅提高。Web Workers 也是如此。不同的操作也可能有很大差异。矩阵乘法使用左侧和右侧结构的方式不同,可能需要一些不同的策略。但我认为最大的收获是:
对于广义的元素矩阵操作,最好的选择是对普通 JS 数组进行单一平面循环,由于它速度快,扩展性好
完整的对比记录:
|
Name |
min |
max |
avg |
p75 |
p99 |
p995 |
|
Add 1×1 (Func) |
63ns |
180ns |
70ns |
74ns |
113ns |
124ns |
|
Add 1×1 (Loop) |
28ns |
210ns |
46ns |
47ns |
142ns |
168ns |
|
Add 1×1 (Loop Prealloc) |
13ns |
137ns |
18ns |
20ns |
56ns |
73ns |
|
Add 1×1 (unrolled) |
7ns |
34ns |
8ns |
8ns |
19ns |
20ns |
|
Add 1×1 (unrolled dynamic) |
7ns |
40ns |
8ns |
7ns |
19ns |
20ns |
|
Add 1×1 (flat) |
9ns |
53ns |
10ns |
10ns |
24ns |
29ns |
|
Add 1×1 (flat col major) |
9ns |
41ns |
10ns |
9ns |
21ns |
22ns |
|
Add 1×1 (flat simple) |
7ns |
46ns |
8ns |
8ns |
18ns |
20ns |
|
Add 1×1 (flat unrolled) |
6ns |
53ns |
7ns |
7ns |
19ns |
22ns |
|
Add 1×1 (F64) |
330ns |
1600ns |
400ns |
397ns |
663ns |
1600ns |
|
Add 1×1 (F32) |
324ns |
554ns |
380ns |
391ns |
506ns |
554ns |
|
Add 1×1 (I32) |
321ns |
1015ns |
390ns |
398ns |
704ns |
1015ns |
|
Add 2×2 (Func) |
144ns |
208ns |
152ns |
158ns |
184ns |
196ns |
|
Add 2×2 (Loop) |
55ns |
163ns |
71ns |
76ns |
125ns |
143ns |
|
Add 2×2 (Loop Prealloc) |
25ns |
65ns |
28ns |
27ns |
45ns |
53ns |
|
Add 2×2 (unrolled) |
11ns |
46ns |
13ns |
12ns |
26ns |
29ns |
|
Add 2×2 (unrolled dynamic) |
11ns |
39ns |
12ns |
12ns |
27ns |
29ns |
|
Add 2×2 (flat) |
14ns |
49ns |
15ns |
15ns |
29ns |
30ns |
|
Add 2×2 (flat col major) |
14ns |
41ns |
15ns |
14ns |
29ns |
32ns |
|
Add 2×2 (flat simple) |
9ns |
54ns |
10ns |
10ns |
23ns |
26ns |
|
Add 2×2 (flat unrolled) |
7ns |
62ns |
8ns |
8ns |
21ns |
23ns |
|
Add 2×2 (F64) |
329ns |
598ns |
393ns |
409ns |
493ns |
598ns |
|
Add 2×2 (F32) |
324ns |
594ns |
391ns |
408ns |
520ns |
594ns |
|
Add 2×2 (I32) |
324ns |
570ns |
390ns |
403ns |
501ns |
570ns |
|
Add 4×4 (Func) |
312ns |
373ns |
329ns |
335ns |
370ns |
373ns |
|
Add 4×4 (Loop) |
122ns |
227ns |
143ns |
151ns |
195ns |
225ns |
|
Add 4×4 (Loop Prealloc) |
61ns |
152ns |
73ns |
78ns |
124ns |
129ns |
|
Add 4×4 (unrolled) |
36ns |
159ns |
59ns |
72ns |
124ns |
130ns |
|
Add 4×4 (unrolled dynamic) |
36ns |
236ns |
67ns |
84ns |
156ns |
181ns |
|
Add 4×4 (flat) |
32ns |
107ns |
40ns |
46ns |
86ns |
94ns |
|
Add 4×4 (flat col major) |
32ns |
79ns |
37ns |
37ns |
61ns |
67ns |
|
Add 4×4 (flat simple) |
18ns |
77ns |
24ns |
28ns |
51ns |
56ns |
|
Add 4×4 (flat unrolled) |
24ns |
136ns |
37ns |
41ns |
84ns |
93ns |
|
Add 4×4 (F64) |
393ns |
1786ns |
490ns |
503ns |
662ns |
1786ns |
|
Add 4×4 (F32) |
396ns |
658ns |
463ns |
489ns |
569ns |
658ns |
|
Add 4×4 (I32) |
372ns |
530ns |
426ns |
443ns |
488ns |
530ns |
|
Add 8×8 (Func) |
694ns |
930ns |
724ns |
731ns |
930ns |
930ns |
|
Add 8×8 (Loop) |
360ns |
807ns |
411ns |
422ns |
744ns |
807ns |
|
Add 8×8 (Loop Prealloc) |
203ns |
444ns |
228ns |
232ns |
348ns |
434ns |
|
Add 8×8 (unrolled) |
92ns |
243ns |
130ns |
142ns |
235ns |
242ns |
|
Add 8×8 (unrolled dynamic) |
89ns |
262ns |
113ns |
119ns |
186ns |
209ns |
|
Add 8×8 (flat) |
97ns |
167ns |
110ns |
113ns |
143ns |
157ns |
|
Add 8×8 (flat col major) |
101ns |
156ns |
114ns |
116ns |
147ns |
153ns |
|
Add 8×8 (flat simple) |
55ns |
159ns |
73ns |
78ns |
125ns |
136ns |
|
Add 8×8 (flat unrolled) |
61ns |
185ns |
81ns |
86ns |
131ns |
144ns |
|
Add 8×8 (F64) |
490ns |
778ns |
621ns |
664ns |
778ns |
778ns |
|
Add 8×8 (F32) |
508ns |
822ns |
620ns |
673ns |
822ns |
822ns |
|
Add 8×8 (I32) |
455ns |
621ns |
539ns |
575ns |
616ns |
621ns |
|
Add 16×16 (Func) |
1798ns |
1942ns |
1836ns |
1843ns |
1942ns |
1942ns |
|
Add 16×16 (Loop) |
1179ns |
1246ns |
1208ns |
1217ns |
1246ns |
1246ns |
|
Add 16×16 (Loop Prealloc) |
710ns |
942ns |
762ns |
768ns |
942ns |
942ns |
|
Add 16×16 (unrolled) |
500ns |
672800ns |
734ns |
600ns |
3400ns |
10500ns |
|
Add 16×16 (unrolled dynamic) |
500ns |
2052000ns |
799ns |
600ns |
6400ns |
10600ns |
|
Add 16×16 (flat) |
400ns |
548ns |
436ns |
447ns |
517ns |
548ns |
|
Add 16×16 (flat col major) |
423ns |
532ns |
453ns |
465ns |
513ns |
532ns |
|
Add 16×16 (flat simple) |
276ns |
405ns |
315ns |
335ns |
393ns |
405ns |
|
Add 16×16 (flat unrolled) |
300ns |
564700ns |
508ns |
400ns |
1000ns |
6100ns |
|
Add 16×16 (F64) |
1024ns |
5425ns |
1311ns |
1334ns |
5425ns |
5425ns |
|
Add 16×16 (F32) |
1148ns |
1784ns |
1345ns |
1422ns |
1784ns |
1784ns |
|
Add 16×16 (I32) |
784ns |
1202ns |
913ns |
966ns |
1202ns |
1202ns |
|
Add 32×32 (Func) |
5274ns |
6599ns |
5495ns |
5605ns |
6599ns |
6599ns |
|
Add 32×32 (Loop) |
5031ns |
5216ns |
5090ns |
5105ns |
5216ns |
5216ns |
|
Add 32×32 (Loop Prealloc) |
2648ns |
2769ns |
2700ns |
2716ns |
2769ns |
2769ns |
|
Add 32×32 (unrolled) |
73800ns |
562500ns |
83976ns |
85200ns |
136400ns |
160600ns |
|
Add 32×32 (unrolled dynamic) |
73000ns |
908200ns |
90772ns |
90900ns |
137900ns |
162600ns |
|
Add 32×32 (flat) |
1985ns |
2900ns |
2222ns |
2276ns |
2900ns |
2900ns |
|
Add 32×32 (flat col major) |
2047ns |
3228ns |
2199ns |
2258ns |
3228ns |
3228ns |
|
Add 32×32 (flat simple) |
1387ns |
1682ns |
1490ns |
1547ns |
1682ns |
1682ns |
|
Add 32×32 (flat unrolled) |
63600ns |
826700ns |
74574ns |
75200ns |
133000ns |
162600ns |
|
Add 32×32 (F64) |
3346ns |
4707ns |
3772ns |
4115ns |
4707ns |
4707ns |
|
Add 32×32 (F32) |
3258ns |
3840ns |
3344ns |
3337ns |
3840ns |
3840ns |
|
Add 32×32 (I32) |
2111ns |
2704ns |
2182ns |
2182ns |
2704ns |
2704ns |
|
Add 64×64 (Func) |
13000ns |
2331200ns |
17451ns |
16300ns |
41900ns |
60700ns |
|
Add 64×64 (Loop) |
14300ns |
362400ns |
20651ns |
19200ns |
52900ns |
110500ns |
|
Add 64×64 (Loop Prealloc) |
9500ns |
372100ns |
10926ns |
10100ns |
25000ns |
35800ns |
|
Add 64×64 (unrolled) |
328700ns |
737300ns |
350104ns |
343900ns |
574500ns |
587000ns |
|
Add 64×64 (unrolled dynamic) |
327600ns |
698800ns |
349201ns |
345400ns |
573900ns |
592400ns |
|
Add 64×64 (flat) |
8512ns |
10514ns |
8775ns |
8715ns |
10514ns |
10514ns |
|
Add 64×64 (flat col major) |
7500ns |
413800ns |
10417ns |
10200ns |
26200ns |
37000ns |
|
Add 64×64 (flat simple) |
6381ns |
7219ns |
6602ns |
6675ns |
7219ns |
7219ns |
|
Add 64×64 (flat unrolled) |
263500ns |
788800ns |
286503ns |
280600ns |
502900ns |
528900ns |
|
Add 64×64 (F64) |
8000ns |
2309700ns |
14203ns |
12700ns |
35300ns |
44800ns |
|
Add 64×64 (F32) |
10500ns |
1101800ns |
18473ns |
21600ns |
66500ns |
101200ns |
|
Add 64×64 (I32) |
8742ns |
9569ns |
9138ns |
9305ns |
9569ns |
9569ns |
|
Add 100×100 (Func) |
30800ns |
512800ns |
40269ns |
38200ns |
105700ns |
218300ns |
|
Add 100×100 (Loop) |
38200ns |
425400ns |
54401ns |
54100ns |
227700ns |
256300ns |
|
Add 100×100 (Loop Prealloc) |
24500ns |
515800ns |
28392ns |
26300ns |
62100ns |
204400ns |
|
Add 100×100 (unrolled) |
829600ns |
1250900ns |
876580ns |
873700ns |
1143900ns |
1157500ns |
|
Add 100×100 (unrolled dynamic) |
816900ns |
1416300ns |
891844ns |
894500ns |
1227700ns |
1288200ns |
|
Add 100×100 (flat) |
15500ns |
701100ns |
23261ns |
21800ns |
54200ns |
194800ns |
|
Add 100×100 (flat col major) |
19800ns |
575300ns |
25090ns |
23500ns |
63000ns |
198500ns |
|
Add 100×100 (flat simple) |
9000ns |
598000ns |
17166ns |
15700ns |
49400ns |
178400ns |
|
Add 100×100 (flat unrolled) |
706400ns |
1760300ns |
764369ns |
758900ns |
1102800ns |
1118900ns |
|
Add 100×100 (F64) |
23200ns |
3328400ns |
35026ns |
33300ns |
82400ns |
231000ns |
|
Add 100×100 (F32) |
25800ns |
1797500ns |
37062ns |
35800ns |
99800ns |
245400ns |
|
Add 100×100 (I32) |
12600ns |
2578300ns |
22470ns |
21600ns |
50300ns |
72200ns |

原文链接:JS快速矩阵计算 – BimAnt