如何用SIMD优化AssemblyScript中的Box Blur算法循环?
AssemblyScript中SIMD优化Box Blur循环方案
核心优化逻辑
原代码逐像素处理RGB分量,每次仅操作单个字节,效率低下。利用WebAssembly SIMD的128位向量能力,可一次并行处理4个像素的对应分量,将循环次数压缩至原有的1/4,同时借助CPU并行运算大幅提升执行速度。
优化后代码
// 预计算直径的浮点倒数,用乘法替代除法提升效率 const invDiameter = 1.0 / diameter; const invDiameterVec = v128.splat(invDiameter as f32); // 初始化向量累加器,存储4个像素的R/G/B累加值 let rAccum = v128.splat(0 as i32); let gAccum = v128.splat(0 as i32); let bAccum = v128.splat(0 as i32); // 按4个像素为一组处理,步长设为4 for (let column: i16 = x + deviationBase + 1; column < x + width - deviationBase; column += 4) { // 计算当前组的加载位置:右侧进入模糊窗口的4个像素、左侧离开窗口的4个像素 const addPixelPos = rowPosition + (column + deviationBase) * 4; const subPixelPos = rowPosition + (column - deviationBase) * 4; // 批量加载4个像素的RGBA数据(128位向量) const addPixels = v128.load<u8>(addPixelPos); const subPixels = v128.load<u8>(subPixelPos); // 从RGBA向量中提取4个像素的R/G/B分量 const addR = v128.shuffle(addPixels, addPixels, [0, 4, 8, 12]); const addG = v128.shuffle(addPixels, addPixels, [1, 5, 9, 13]); const addB = v128.shuffle(addPixels, addPixels, [2, 6, 10, 14]); const subR = v128.shuffle(subPixels, subPixels, [0, 4, 8, 12]); const subG = v128.shuffle(subPixels, subPixels, [1, 5, 9, 13]); const subB = v128.shuffle(subPixels, subPixels, [2, 6, 10, 14]); // 将u8分量转换为i32,避免累加溢出 const addR_i32 = v128.extend_low_u8_to_i32(addR); const addG_i32 = v128.extend_low_u8_to_i32(addG); const addB_i32 = v128.extend_low_u8_to_i32(addB); const subR_i32 = v128.extend_low_u8_to_i32(subR); const subG_i32 = v128.extend_low_u8_to_i32(subG); const subB_i32 = v128.extend_low_u8_to_i32(subB); // 并行执行累加/减法操作 rAccum = v128.add(rAccum, addR_i32); gAccum = v128.add(gAccum, addG_i32); bAccum = v128.add(bAccum, addB_i32); rAccum = v128.sub(rAccum, subR_i32); gAccum = v128.sub(gAccum, subG_i32); bAccum = v128.sub(bAccum, subB_i32); // 将累加值转换为浮点,乘以倒数计算平均值 const rAvg_f32 = v128.mul(v128.convert_i32x4_to_f32x4(rAccum), invDiameterVec); const gAvg_f32 = v128.mul(v128.convert_i32x4_to_f32x4(gAccum), invDiameterVec); const bAvg_f32 = v128.mul(v128.convert_i32x4_to_f32x4(bAccum), invDiameterVec); // 将浮点平均值转换回u8 const rAvg_u8 = v128.convert_f32x4_to_u8x16(rAvg_f32); const gAvg_u8 = v128.convert_f32x4_to_u8x16(gAvg_f32); const bAvg_u8 = v128.convert_f32x4_to_u8x16(bAvg_f32); // 重新组合RGBA向量(A分量设为255) const outputPixels = v128.shuffle(rAvg_u8, gAvg_u8, [ 0, 1, 2, 255, // 第一个像素:R, G, B, A(255) 4, 5, 6, 255, // 第二个像素 8, 9, 10, 255, // 第三个像素 12, 13, 14, 255// 第四个像素 ]); // 批量写入4个像素的结果 v128.store(rowPosition + column * 4, outputPixels); } // 处理剩余不足4个的像素(避免宽度非4倍数时的边界错误) let rScalar: i32 = v128.extract_lane(rAccum, 0); let gScalar: i32 = v128.extract_lane(gAccum, 0); let bScalar: i32 = v128.extract_lane(bAccum, 0); for (; column < x + width - deviationBase; column++) { rScalar += load<u8>(rowPosition + (column + deviationBase) * 4); gScalar += load<u8>(rowPosition + (column + deviationBase) * 4 + 1); bScalar += load<u8>(rowPosition + (column + deviationBase) * 4 + 2); rScalar -= load<u8>(rowPosition + (column - deviationBase) * 4); gScalar -= load<u8>(rowPosition + (column - deviationBase) * 4 + 1); bScalar -= load<u8>(rowPosition + (column - deviationBase) * 4 + 2); store<u8>(rowPosition + column * 4, (rScalar / diameter) as u8); store<u8>(rowPosition + column * 4 + 1, (gScalar / diameter) as u8); store<u8>(rowPosition + column * 4 + 2, (bScalar / diameter) as u8); }
关键优化点解释
- SIMD启用:编译时必须添加
--simd参数,AssemblyScript才会生成SIMD指令,目标环境需支持WebAssembly SIMD(现代浏览器、Node.js均兼容)。 - 向量化累加:用
v128向量替代标量存储R/G/B累加值,一次并行处理4个像素的累加操作,避免循环迭代开销。 - 批量加载/存储:
v128.load/v128.store一次处理16字节(4个RGBA像素),替代原代码多次单字节操作,大幅减少内存访问次数。 - 分量提取:通过
v128.shuffle从RGBA向量中分离R/G/B分量,确保向量运算仅针对目标分量进行。 - 溢出避免:将u8分量转换为i32类型累加,避免u8范围(0-255)导致的溢出错误,计算完成后再转换回u8。
- 除法优化:预计算直径的浮点倒数,用向量乘法替代除法,SIMD乘法的执行效率远高于除法。
- 边界处理:针对宽度非4倍数的情况,单独处理剩余像素,保证结果正确性。
内容的提问来源于stack exchange,提问作者Kevin Baving
相关产品推荐
相关产品推荐

