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