You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AssemblyScript Wasm基数排序比纯JS慢4倍,求优化方案

AssemblyScript WASM 性能优化排查指南

问题背景

已将JS基数排序顶点变换基准测试移植到AssemblyScript WASM,完成了noAssert、运行时存根、optimizeLevel 3、启用SIMD、复用全局变量、乒乓缓冲区、减少批量复制、显式StaticArray计数等常规优化,但Wasm版本运行速度仍远慢于原JS版本,需排查遗漏的优化点。

现有代码实现

assembly/radix.ts

// 8-bit radix parameters
const BUCKETS = 256;

// Temp buffers used by the sort. Stored globally to avoid any allocations in the hot path.
let b_keys: Uint32Array | null = null;
let b_indices: Uint32Array | null = null;

// One single StaticArray for bucket counts — no GC, no reallocations ever.
const b_counts = new StaticArray<i32>(BUCKETS);

/**
 * Sorts two parallel Uint32Array buffers in place by their keys.
 */
export function radixSort(keys: Uint32Array, indices: Uint32Array): void {
    const n = keys.length;

    // Allocate scratch buffers once, or grow them if needed
    if (b_keys === null || b_keys!.length < n) {
        b_keys = new Uint32Array(n);
        b_indices = new Uint32Array(n);
    }
    const tempKeys = b_keys!;
    const tempIndices = b_indices!;

    // Do one pass per byte
    for (let shift = 0; shift < 32; shift += 8) {
        // 1) Zero the counts
        for (let b = 0; b < BUCKETS; ++b) {
            b_counts[b] = 0;
        }

        // 2) Counting pass
        for (let i = 0; i < n; ++i) {
            const k = keys[i];
            b_counts[(k >>> shift) & (BUCKETS - 1)]++;
        }

        // 3) Prefix sum → bucket starting indices
        for (let b = 1; b < BUCKETS; ++b) {
            b_counts[b] += b_counts[b - 1];
        }

        // 4) Placement pass (in reverse) to keep stability
        for (let i = n - 1; i >= 0; --i) {
            const k = keys[i];
            const bucket = (k >>> shift) & (BUCKETS - 1);
            const dst = --b_counts[bucket];
            tempKeys[dst] = k;
            tempIndices[dst] = indices[i];
        }

        // 5) Bulk-copy back into the original arrays
        memory.copy(
            keys.dataStart,       // dest ptr
            tempKeys.dataStart,   // src ptr
            <usize>n << 2         // n * 4 bytes
        );
        memory.copy(
            indices.dataStart,
            tempIndices.dataStart,
            <usize>n << 2
        );
    }
}

/**
 * Float→sortable-u32 conversion (unchanged).
 */
@inline
export function f32ToSortable(val: f32): u32 {
    const iv = reinterpret<u32>(val);
    // flip bits so negatives sort before positives
    return ((iv >> 31) & 1) ? ~iv : (iv | 0x8000_0000);
}

assembly/simd.ts

// This file now contains both SIMD and Scalar versions of the transform function,
// selected at compile-time by the ASC_SIMD flag in your package.json.

@inline
export function transform_vertex_simd(vertexPtr: usize, matrixPtr: usize): f32 {

    // --- The 'if (ASC_SIMD)' block is evaluated at COMPILE TIME ---
    if (ASC_SIMD) {
        // --- SIMD PATH ---
        // This code will only be included in your 'release-simd.wasm' build.
        // It requires the matrix to be transposed in JavaScript.

        const vert_x = load<f32>(vertexPtr, 0);
        const vert_y = load<f32>(vertexPtr, 4);
        const vert_z = load<f32>(vertexPtr, 8);
        const v_vertex = f32x4(vert_x, vert_y, vert_z, 1.0);

        const v_row2 = v128.load(matrixPtr, 32);

        const products = f32x4.mul(v_vertex, v_row2);

        const sum_xy = f32x4.extract_lane(products, 0) + f32x4.extract_lane(products, 1);
        const sum_zw = f32x4.extract_lane(products, 2) + f32x4.extract_lane(products, 3);
        const res_z = sum_xy + sum_zw;

        return res_z;

    } else {
        // --- SCALAR PATH ---
        // This code will only be included in your 'release-scalar.wasm' build.
        // It uses the original, non-transposed matrix.

        const x = load<f32>(vertexPtr, 0);
        const y = load<f32>(vertexPtr, 4);
        const z = load<f32>(vertexPtr, 8);
        const w: f32 = 1.0; // w is implicitly 1

        // Direct calculation of Z_out = m[8]*x + m[9]*y + m[10]*z + m[11]*w
        // using the original column-major matrix layout.
        const m8 = load<f32>(matrixPtr, 32);
        const m9 = load<f32>(matrixPtr, 36);
        const m10 = load<f32>(matrixPtr, 40);
        const m11 = load<f32>(matrixPtr, 44);

        // This scalar version is simplified to match the SIMD version's logic exactly.
        // It calculates only the Z-depth from the original (non-transposed) matrix.
        const res_z = m8 * x + m9 * y + m10 * z + m11 * w;

        return res_z;
    }
}

assembly/index.ts

// assembly/index.ts (Corrected Benchmarking Logic)

import { transform_vertex_simd } from "./simd"; // We now only import the SIMD version
import { radixSort, f32ToSortable } from "./radix";
import { lastProfile, ProfileData } from "./profiler";

let inputVerticesPtr: usize = 0;
let inputMatrixPtr: usize = 0;
let outputVerticesPtr: usize = 0;

const F32_SIZE: u32 = 4;
const VERTEX_SZ: u32 = 3 * F32_SIZE;

export function getProfileReport(): ProfileData {
    return lastProfile;
}

export function alloc(numVertices: i32): StaticArray<u32> {
    const vc = <u32>numVertices;
    inputVerticesPtr = heap.alloc(vc * VERTEX_SZ);
    inputMatrixPtr = heap.alloc(16 * F32_SIZE);
    outputVerticesPtr = heap.alloc(vc * VERTEX_SZ);
    const out = new StaticArray<u32>(2);
    out[0] = <u32>inputVerticesPtr;
    out[1] = <u32>inputMatrixPtr;
    return out;
}

// The sort function now runs ONLY ONCE. The repetition is handled in JavaScript.
export function sort(numVertices: i32): u32 {
    const vc = <u32>numVertices;
    const keys = new Uint32Array(vc);
    const indices = new Uint32Array(vc);

    // Stage 1: Key Calculation
    const t1 = Date.now();
    for (let i: u32 = 0; i < vc; ++i) {
        const ptr = inputVerticesPtr + i * VERTEX_SZ;
        // The matrix pointer is now passed directly to the transform function
        const depth = transform_vertex_simd(ptr, inputMatrixPtr);
        keys[i] = f32ToSortable(depth);
        indices[i] = i;
    }
    lastProfile.keyCalculation = Date.now() - t1;

    // Stage 2: Radix Sort
    const t2 = Date.now();
    radixSort(keys, indices);
    lastProfile.radixSort = Date.now() - t2;

    // Stage 3: Reordering
    const t3 = Date.now();
    for (let i: u32 = 0; i < vc; ++i) {
        const orig = indices[i];
        const src = inputVerticesPtr + orig * VERTEX_SZ;
        const dst = outputVerticesPtr + i * VERTEX_SZ;
        memory.copy(dst, src, VERTEX_SZ);
    }
    lastProfile.reordering = Date.now() - t3;

    lastProfile.total = lastProfile.keyCalculation + lastProfile.radixSort + lastProfile.reordering;
    return <u32>outputVerticesPtr;
}

assembly/profiler.ts

// This class will hold our detailed performance report.
// The ESM bindings will automatically convert this to a JS object.
export class ProfileData {
  keyCalculation: i64 = 0;
  radixSort: i64 = 0;
  reordering: i64 = 0;
  total: i64 = 0;
}

// A global instance to store the data from the last run.
export const lastProfile = new ProfileData();

关键优化遗漏点及修复方案

1. 消除数组边界检查开销

当前使用的Uint32Array即使开启noAssert仍可能保留隐式边界检查,改用StaticArray<u32>直接操作内存,彻底消除检查:

// 修改radixSort的参数与全局buffer类型
let b_keys: StaticArray<u32> | null = null;
let b_indices: StaticArray<u32> | null = null;

export function radixSort(keys: StaticArray<u32>, indices: StaticArray<u32>): void {
    const n = keys.length;
    // 后续数组访问直接用下标,无边界检查
}

2. 充分利用SIMD求和指令

当前SIMD路径手动提取lane相加浪费性能,改用SIMD水平求和指令:

// 替换SIMD路径求和代码
const products = f32x4.mul(v_vertex, v_row2);
const sum1 = f32x4.hadd(products, products);
const sum2 = f32x4.hadd(sum1, sum1);
const res_z = f32x4.extract_lane(sum2, 0);

3. 减少内存拷贝次数

基数排序每轮的双向拷贝是性能瓶颈,改用双缓冲指针切换策略:

// radixSort中引入源/目标指针切换
let srcKeys = keys;
let srcIndices = indices;
let dstKeys = tempKeys;
let dstIndices = tempIndices;

for (let shift = 0; shift < 32; shift += 8) {
    // 计数、前缀和、放置阶段写入dstKeys/dstIndices
    // 交换源和目标,下一轮反向处理
    [srcKeys, dstKeys] = [dstKeys, srcKeys];
    [srcIndices, dstIndices] = [dstIndices, srcIndices];
}

// 若为奇数轮,最后一次拷贝回原数组
if (srcKeys !== keys) {
    memory.copy(keys.dataStart, srcKeys.dataStart, <usize>n << 2);
    memory.copy(indices.dataStart, srcIndices.dataStart, <usize>n << 2);
}

4. 优化内存访问模式

  • 将顶点数据从结构数组(AoS)转为数组结构(SoA),提升缓存命中率;
  • 重排阶段分块处理,利用CPU缓存行特性减少缓存失效。

5. 强化编译优化选项

确保编译时添加以下参数:

asc assembly/index.ts --target release --optimizeLevel 3 --shrinkLevel 2 --noAssert --runtime stub --enable simd --disable bulk-memory
  • --shrinkLevel 2:进一步压缩代码消除冗余;
  • --disable bulk-memory:关闭未使用的特性,提升编译器优化空间;
  • 确认ASC_SIMD=1环境变量已设置,确保SIMD代码被编译。

6. 修正基准测试精度

  • 确保JS与Wasm使用完全相同的输入数据;
  • 改用performance.now()替代Date.now()提升计时精度;
  • 排除Wasm模块加载、内存初始化时间,仅统计核心逻辑运行时长。

内容的提问来源于stack exchange,提问作者Sidhant Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 16:52:32