ndarray vs ndarray-ops vs ndarray-pack vs ndarray-scratch
JavaScript 多维数组与数值计算核心工具包对比
ndarrayndarray-opsndarray-packndarray-scratch

JavaScript 多维数组与数值计算核心工具包对比

ndarrayndarray-opsndarray-packndarray-scratch 是围绕 ndarray 核心数据结构构建的一组轻量级 JavaScript 库,用于在浏览器或 Node.js 环境中高效处理多维数值数组。ndarray 本身定义了带步长(strides)、偏移(offset)和形状(shape)的通用数组视图;ndarray-ops 提供对 ndarray 实例的原地数学与逻辑运算;ndarray-pack 用于将嵌套 JavaScript 数组转换为 ndarrayndarray-scratch 则提供临时 ndarray 的分配与回收机制,避免频繁内存分配。这些库共同构成一个低开销、无依赖的科学计算基础层,适用于图像处理、物理仿真、机器学习前处理等场景。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
ndarray1,372,1181,24522.5 kB221 个月前MIT
ndarray-ops069-512 年前MIT
ndarray-pack010-110 年前MIT
ndarray-scratch012-511 年前MIT

ndarray 生态核心四件套:数据结构、运算、构造与内存管理深度解析

在前端进行高性能数值计算(如图像滤镜、物理引擎、信号处理)时,原生 JavaScript 数组往往效率低下且缺乏多维语义。ndarray 及其配套工具包提供了一套轻量、组合式、零依赖的解决方案。本文从真实工程角度,对比 ndarrayndarray-opsndarray-packndarray-scratch 的设计意图、使用场景与协作方式。

🧱 核心角色:谁负责什么?

这四个包各司其职,共同构成一个完整的数值计算基础层:

  • ndarray:定义多维数组的数据结构,类似 NumPy 的 ndarray,但更轻量。
  • ndarray-ops:提供对 ndarray原地运算(in-place operations),如 add, mul, sin 等。
  • ndarray-pack:将普通 JS 嵌套数组打包ndarray,用于初始化。
  • ndarray-scratch:管理临时数组的生命周期,避免频繁分配/释放内存。

它们不是互斥选项,而是协同工作的组件。典型流程是:用 ndarray-pack 构造输入 → 用 ndarray-ops 执行计算 → 用 ndarray-scratch 管理中间结果。

📦 数据构造:如何从 JS 数组变成 ndarray?

ndarray-pack:一键打包嵌套数组

当你有静态数据(如测试用例、配置矩阵),ndarray-pack 是最直接的方式:

import pack from 'ndarray-pack';

const data = [[1, 2, 3], [4, 5, 6]];
const arr = pack(data);
// arr.shape = [2, 3]
// arr.data = Float64Array([1, 2, 3, 4, 5, 6])

ndarray:手动构造(更灵活)

若需控制底层类型、步长或偏移(如创建子视图),直接使用 ndarray 构造函数:

import ndarray from 'ndarray';

const buffer = new Float32Array(12); // 12 个元素
const arr = ndarray(buffer, [3, 4]); // 3x4 矩阵
// 或创建转置视图
const transposed = ndarray(buffer, [4, 3], [1, 4]); // strides=[1,4]

💡 注意:ndarray-pack 内部也调用 ndarray,但它帮你处理了类型推断和内存分配。

ndarray-scratch:临时构造(带回收)

在循环中反复创建相同形状的数组?用 ndarray-scratch 避免内存抖动:

import scratch from 'ndarray-scratch';

// 分配一个 100x100 的临时数组
const temp = scratch.malloc([100, 100], 'float32');
// ... 执行计算 ...
scratch.free(temp); // 归还内存,下次 malloc 可能复用

ndarray-ops:不负责构造

ndarray-ops 不提供任何构造函数,它只操作已存在的 ndarray 实例。尝试用它创建数组会失败。

⚙️ 数值运算:如何高效修改数据?

ndarray-ops:原地运算主力

所有运算直接修改 ndarray.data,无中间拷贝:

import ops from 'ndarray-ops';
import ndarray from 'ndarray';

const a = ndarray(new Float32Array([1, 2, 3]));
const b = ndarray(new Float32Array([4, 5, 6]));

ops.add(a, b); // a.data 变为 [5, 7, 9]
ops.mulseq(a, 2); // a.data 变为 [10, 14, 18]

支持标量、数组、函数作为操作数,例如:

ops.sin(a); // 对每个元素应用 Math.sin
ops.gt(a, 10); // 元素 >10 为 1,否则为 0

其他包:不提供运算

  • ndarray:仅提供 .get() / .set() 方法,逐元素访问,不适合批量运算。
  • ndarray-pack:纯构造工具,无运算能力。
  • ndarray-scratch:纯内存管理,无运算能力。

⚠️ 警告:若需非原地运算(如保留输入不变),必须先用 ndarray-scratchndarray 创建副本,再用 ndarray-ops 修改副本。

🧼 内存管理:如何避免 GC 压力?

ndarray-scratch:临时数组池

在帧循环(如 WebGL 渲染)或递归算法中,频繁分配小数组会导致 GC 卡顿。ndarray-scratch 通过对象池缓解此问题:

function processFrame(input) {
  const temp1 = scratch.malloc([input.shape[0]]);
  const temp2 = scratch.malloc([input.shape[0]]);
  
  ops.add(temp1, input, 10); // 加偏移
  ops.mul(temp2, temp1, 0.5); // 缩放
  
  // 使用 temp2 ...
  
  scratch.free(temp1);
  scratch.free(temp2); // 内存归还池中
}

其他包:无内存管理

  • ndarray:每次 new ndarray() 都分配新内存,无回收机制。
  • ndarray-pack:每次调用都新建 TypedArray,适合一次性数据。
  • ndarray-ops:不涉及内存分配。

🔁 典型工作流:四者如何配合?

假设你要实现一个简单的图像亮度调整:

import pack from 'ndarray-pack';
import ops from 'ndarray-ops';
import scratch from 'ndarray-scratch';

// 1. 用 ndarray-pack 初始化图像数据(假设 RGBA)
const imageData = pack([
  [[255, 0, 0, 255], [0, 255, 0, 255]],
  [[0, 0, 255, 255], [255, 255, 0, 255]]
]); // shape: [2, 2, 4]

// 2. 用 ndarray-scratch 创建临时亮度通道
const brightness = scratch.malloc([2, 2]);

// 3. 用 ndarray-ops 计算亮度 (0.299*R + 0.587*G + 0.114*B)
const r = imageData.pick(null, null, 0); // R 通道视图
const g = imageData.pick(null, null, 1); // G 通道视图
const b = imageData.pick(null, null, 2); // B 通道视图

ops.mul(brightness, r, 0.299);
ops.addeq(brightness, ops.mul(g, 0.587));
ops.addeq(brightness, ops.mul(b, 0.114));

// 4. 调整亮度(+50)
ops.addeq(brightness, 50);

// 5. 释放临时数组
scratch.free(brightness);

在此流程中:

  • ndarray-pack 负责初始化复杂嵌套数据。
  • ndarray(隐式)提供通道视图(通过 .pick())。
  • ndarray-ops 执行所有数学运算
  • ndarray-scratch 管理临时亮度数组的生命周期。

🚫 常见误区与避坑指南

误区 1:用 ndarray 代替 ndarray-pack 初始化嵌套数据

// ❌ 错误:ndarray 不解析嵌套结构
const bad = ndarray([[1,2],[3,4]]); // data 变成 [[1,2],[3,4]],非 TypedArray!

// ✅ 正确:用 ndarray-pack
const good = pack([[1,2],[3,4]]);

误区 2:忘记 ndarray-ops 是原地操作

// ❌ 危险:a 被意外修改
ops.add(a, b); // a 改变!

// ✅ 安全:先复制
const result = scratch.malloc(a.shape);
ops.assign(result, a); // 复制 a 到 result
ops.add(result, b); // 修改 result,a 不变

误区 3:在长期持有对象中使用 ndarray-scratch

scratch.malloc() 返回的数组可能被后续 malloc 覆盖。仅用于短期临时变量,绝不可存储到全局状态或返回给调用者。

📊 总结:何时使用哪个包?

场景推荐包原因
定义多维数组结构或创建视图ndarray提供 shape/stride/data 核心抽象
执行数学/逻辑运算ndarray-ops原地操作,零内存拷贝
从 JS 嵌套数组初始化ndarray-pack自动推断维度与类型
循环/递归中的临时数组ndarray-scratch内存池减少 GC 压力

💡 最终建议

  • 不要单独使用:这四个包设计为组合使用。ndarray 是基石,其他包在其上构建。
  • 性能关键路径:优先用 ndarray-ops + ndarray-scratch,避免 .get()/.set() 逐元素访问。
  • 初始化简单数据:用 ndarray-pack;需要精细控制内存布局时,用 ndarray 手动构造。
  • 临时数组必回收:凡用 scratch.malloc(),务必配对 scratch.free(),否则内存泄漏。

这套工具虽小,却为前端数值计算提供了接近原生的效率,是构建高性能科学计算应用的可靠基石。

如何选择: ndarray vs ndarray-ops vs ndarray-pack vs ndarray-scratch

  • ndarray:

    选择 ndarray 作为多维数组的基础数据结构。它不包含具体运算逻辑,但提供了 shape、stride、data 等核心属性,是其他 ndarray 生态包的依赖基础。如果你需要自定义数组视图(如切片、转置)或构建上层计算库,必须使用它。

  • ndarray-ops:

    选择 ndarray-ops 当你需要对 ndarray 执行高效的原地数学运算(如加法、乘法、三角函数)或逻辑操作(如比较、条件赋值)。所有操作直接修改底层 TypedArray,避免内存拷贝,适合性能敏感的数值密集型任务。

  • ndarray-pack:

    选择 ndarray-pack 当你有一个嵌套的 JavaScript 数组(如 [[1, 2], [3, 4]])并希望快速将其转换为 ndarray 实例。它会自动推断维度和类型,并分配连续内存,是初始化静态数据最便捷的方式。

  • ndarray-scratch:

    选择 ndarray-scratch 当你需要频繁创建临时 ndarray 用于中间计算(如卷积、矩阵乘法),并希望复用内存以减少垃圾回收压力。它通过对象池管理生命周期,适合循环或递归中的短期数组使用。

ndarray的README

ndarray

Modular multidimensional arrays for JavaScript.

browser support

build status

stable

Browse a number of ndarray-compatible modules in the scijs documentation
Coming from MATLAB or numpy? See: scijs/ndarray for MATLAB users
Big list of ndarray modules

Introduction

ndarrays provide higher dimensional views of 1D arrays. For example, here is how you can turn a length 4 typed array into an nd-array:

var mat = ndarray(new Float64Array([1, 0, 0, 1]), [2,2])

//Now:
//
// mat = 1 0
//       0 1
//

Once you have an nd-array you can access elements using .set and .get. For example, here is an implementation of Conway's game of life using ndarrays:

function stepLife(next_state, cur_state) {

  //Get array shape
  var nx = cur_state.shape[0], 
      ny = cur_state.shape[1]

  //Loop over all cells
  for(var i=1; i<nx-1; ++i) {
    for(var j=1; j<ny-1; ++j) {

      //Count neighbors
      var n = 0
      for(var dx=-1; dx<=1; ++dx) {
        for(var dy=-1; dy<=1; ++dy) {
          if(dx === 0 && dy === 0) {
            continue
          }
          n += cur_state.get(i+dx, j+dy)
        }
      }
      
      //Update state according to rule
      if(n === 3 || n === 3 + cur_state.get(i,j)) {
        next_state.set(i,j,1)
      } else {
        next_state.set(i,j,0)
      }
    }
  }
}

You can also pull out views of ndarrays without copying the underlying elements. Here is an example showing how to update part of a subarray:

var x = ndarray(new Float32Array(25), [5, 5])
var y = x.hi(4,4).lo(1,1)

for(var i=0; i<y.shape[0]; ++i) {
  for(var j=0; j<y.shape[1]; ++j) {
    y.set(i,j,1)
  }
}

//Now:
//    x = 0 0 0 0 0
//        0 1 1 1 0
//        0 1 1 1 0
//        0 1 1 1 0
//        0 0 0 0 0

ndarrays can be transposed, flipped, sheared and sliced in constant time per operation. They are useful for representing images, audio, volume graphics, matrices, strings and much more. They work both in node.js and with browserify.

Install

Install the library using npm:

npm install ndarray

You can also use ndarrays in a browser with any tool that follows the CommonJS/node module conventions. The most direct way to do this is to use browserify. If you want live-reloading for faster debugging, check out beefy.

API

Once you have ndarray installed, you can use it in your project as follows:

var ndarray = require("ndarray")

Constructor

ndarray(data[, shape, stride, offset])

The default module.exports method is the constructor for ndarrays. It creates an n-dimensional array view wrapping an underlying storage type

  • data is a 1D array storage. It is either an instance of Array, a typed array, or an object that implements get(), set(), .length
  • shape is the shape of the view (Default: data.length)
  • stride is the resulting stride of the new array. (Default: row major)
  • offset is the offset to start the view (Default: 0)

Returns an n-dimensional array view of the buffer

Members

The central concept in ndarray is the idea of a view. The way these work is very similar to SciPy's array slices. Views are affine projections to 1D storage types. To better understand what this means, let's first look at the properties of the view object. It has exactly 4 variables:

  • array.data - The underlying 1D storage for the multidimensional array
  • array.shape - The shape of the typed array
  • array.stride - The layout of the typed array in memory
  • array.offset - The starting offset of the array in memory

Keeping a separate stride means that we can use the same data structure to support both row major and column major storage

Element Access

To access elements of the array, you can use the set/get methods:

array.get(i,j,...)

Retrieves element i,j,... from the array. In psuedocode, this is implemented as follows:

function get(i,j,...) {
  return this.data[this.offset + this.stride[0] * i + this.stride[1] * j + ... ]
}

array.set(i,j,...,v)

Sets element i,j,... to v. Again, in psuedocode this works like this:

function set(i,j,...,v) {
  return this.data[this.offset + this.stride[0] * i + this.stride[1] * j + ... ] = v
}

array.index(i,j, ...)

Retrieves the index of the cell in the underlying ndarray. In JS,

function index(i,j, ...) {
  return this.offset + this.stride[0] * i + this.stride[1] * j + ...
}

Properties

The following properties are created using Object.defineProperty and do not take up any physical memory. They can be useful in calculations involving ndarrays

array.dtype

Returns a string representing the undelying data type of the ndarray. Excluding generic data stores these types are compatible with typedarray-pool. This is mapped according to the following rules:

Data typeString
Int8Array"int8"
Int16Array"int16"
Int32Array"int32"
Uint8Array"uint8"
Uint16Array"uint16"
Uint32Array"uint32"
BigInt64Array"bigint64"
BigUint64Array"biguint64"
Float16Array"float16"
Float32Array"float32"
Float64Array"float64"
Array"array"
Uint8ArrayClamped"uint8_clamped"
Buffer"buffer"
Other"generic"

Generic arrays access elements of the underlying 1D store using get()/set() instead of array accessors.

array.size

Returns the size of the array in logical elements.

array.order

Returns the order of the stride of the array, sorted in ascending length. The first element is the first index of the shortest stride and the last is the index the longest stride.

array.dimension

Returns the dimension of the array.

Slicing

Given a view, we can change the indexing by shifting, truncating or permuting the strides. This lets us perform operations like array reversals or matrix transpose in constant time (well, technically O(shape.length), but since shape.length is typically less than 4, it might as well be). To make life simpler, the following interfaces are exposed:

array.lo(i,j,k,...)

This creates a shifted view of the array. Think of it as taking the upper left corner of the image and dragging it inward by an amount equal to (i,j,k...).

array.hi(i,j,k,...)

This does the dual of array.lo(). Instead of shifting from the top-left, it truncates from the bottom-right of the array, returning a smaller array object. Using hi and lo in combination lets you select ranges in the middle of an array.

Note: hi and lo do not commute. In general:

a.hi(3,3).lo(3,3)  !=  a.lo(3,3).hi(3,3)

array.step(i,j,k...)

Changes the stride length by rescaling. Negative indices flip axes. For example, here is how you create a reversed view of a 1D array:

var reversed = a.step(-1)

You can also change the step size to be greater than 1 if you like, letting you skip entries of a list. For example, here is how to split an array into even and odd components:

var evens = a.step(2)
var odds = a.lo(1).step(2)

array.transpose(p0, p1, ...)

Finally, for higher dimensional arrays you can transpose the indices without replicating the data. This has the effect of permuting the shape and stride values and placing the result in a new view of the same data. For example, in a 2D array you can calculate the matrix transpose by:

M.transpose(1, 0)

Or if you have a 3D volume image, you can shift the axes using more generic transformations:

volume.transpose(2, 0, 1)

array.pick(p0, p1, ...)

You can also pull out a subarray from an ndarray by fixing a particular axis. The way this works is you specify the direction you are picking by giving a list of values. For example, if you have an image stored as an nxmx3 array you can pull out the channel as follows:

var red   = image.pick(null, null, 0)
var green = image.pick(null, null, 1)
var blue  = image.pick(null, null, 2)

As the above example illustrates, passing a negative or non-numeric value to a coordinate in pick skips that index.

More information

For more discussion about ndarrays, here are some talks, tutorials and articles about them:

License

(c) 2013-2016 Mikola Lysenko. MIT License