webassembly006 SIMD 矢量运算

        提高wasm性能的途径包括 : 编译期优化标志,使用多线程,使用SIMD内在函数()和wasm_webgpu等 SIMD是其中一个方向。

SIMD

         SIMD(单指令多数据流)即一条指令可以一次处理多个数据,属于数据级并行优化手段。SIMD在X86、ARM CPU架构下都有相应的指令集实现。Flynn分类法根据指令和数据进入CPU的方式,将计算机架构分为四种不同的类型

  • 单指令流单数据流(SISD, Single Instruction stream Single Data stream)
  • 单指令流多数据流(SIMD, Single Instruction stream Multiple Data stream)
  • 多指令流单数据流(MISD, Multiple Instruction stream Single Data stream)
  • 多指令流多数据流(MIMD, Multiple Instruction stream Multiple Data stream)
Data Stream
SingleMultiple
Instruction
  Stream
SingleSISD

a_1+b_1

SIMD

a_1+b_1
a_2+b_2
a_3+b_3
MultipleSIMT

a_1+b_1
a_1*b_1
a_1-b_1
MIMD

a_1+b_1
a_2*b_2
a_3-b_3

SIMD Elements per Data Type

                由于通常没有内建的128bit和256bit数据类型,SIMD指令使用自己构建的数据类型,这些类型以union实现,这些数据类型可以称作向量,一般来说,MMX指令是__m64 类型的数据,SSE是__m128类型(一次操作128bit数据,128bit = 16byte = 4*float)的数据等等。

charshortintint64_tfloatdouble
MMX842100
SSE000040
SSE21684242
AVX1684284
AVX232168484
AVX512F3216168168
AVX512BW6432168168
Data typeIntrinsic prefex
SSE
__m128 (float) __m128d (double) __m128i (int)
_mm_
AVX __m256 (float) __m256d (double) __m256i (int)_mm256_
AVX512 __m512 (float) __m512d (double) __m512i (int)_mm512_

函数指南

x86 SIMD 内在函数的头文件 immintrin.h

  • 当前,通常可以只使用<immintrin.h>. 它包括一切。
  • 从历史上看(在immintrin.h拉入所有内容之前),您必须手动包含所需的最高级别内在函数的标头。这对于 MSVC 和 ICC 仍然有用,可以阻止您使用不需要的指令集。
<mmintrin.h>  MMX
<xmmintrin.h> SSE
<emmintrin.h> SSE2
<pmmintrin.h> SSE3
<tmmintrin.h> SSSE3
<smmintrin.h> SSE4.1
<nmmintrin.h> SSE4.2
<ammintrin.h> SSE4A
<wmmintrin.h> AES
<immintrin.h> AVX, AVX2, FMA

例子

float input1[4] = { 1.2f, 3.5f, 1.7f, 2.8f };
float input2[4] = { -0.7f, 2.6f, 3.3f, -0.8f };
float output[4];
__m128 a = _mm_load_ps(input1);// https://www.csie.ntu.edu.tw/~r89004/hive/sse/page_4.html 要注意的是,這裡是假設這兩個浮點數陣列都是對齊在 16 bytes 的邊上。如果不是的話,就不能用 _mm_load_ps 這個 intrinsic 來載入,而要改用 _mm_loadu_ps 這個 intrinsic。它是專門用來處理沒有對齊在 16 bytes 邊上的資料的。但是,它的速度會比較慢。
__m128 b = _mm_load_ps(input2);
__m128 t = _mm_add_ps(a, b);
_mm_store_ps(output, t);

另外,因為 x86 的 little endian 特性,位址較低的 byte 會放在暫存器的右邊。也就是說,若以上面的 input1 為例,在載入到 XMM 暫存器後,暫存器中的 DATA0 會是 1.2,而 DATA1 是 3.5,DATA2 是 1.7,DATA3 是 2.8。如果需要以相反的順序載入的話,可以用 _mm_loadr_ps 這個 intrinsic。當然,在這個例子中,順序並不影響結果,所以不需要利用這個 intrinsic。

一般來說,宣告一個 float 的陣列,並不會對齊在 16 bytes 的邊上。如果希望它會對齊在 16 bytes 的邊上,以便利用 SSE 指令的話,Visual C++ 6.0 Processor Pack 和 Intel C++ compiler 都可以指定對齊方式。指定的方式如下:

__declspec(align(16)) float input1[4];

WebAssembly+SIMD

  • https://emscripten.org/docs/porting/simd.html

  • 目前仅支持 SSE1、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2 和 128 位 AVX 指令集。

  • 使用 WebAssembly LLVM 后端时,Emscripten 支持WebAssembly SIMD 建议。要启用 SIMD,请在编译时传递 -msimd128 标志。这还将打开 LLVM 的自动矢量化通道,因此无需修改源代码即可从 SIMD 中受益。

  • 在源代码级别,可以使用GCC/Clang SIMD 矢量扩展,并将在可能的情况下降低为 WebAssembly SIMD 指令。此外,还有一个可以使用的可移植内在函数头文件#include <wasm_simd128.h>

  • 内在函数头的单独文档正在开发中,但其用法很简单,其源代码可以在wasm_simd128.h中找到。这些内在函数正在与 SIMD 提案同时积极开发,不应被视为比提案本身更稳定。请注意,大多数引擎还需要一个额外的标志来启用 SIMD。例如,Node 需要–experimental-wasm-simd。

  • Emscripten 支持通过将-msse指令传递给编译器并包含头文件<xmmintrin.h>来编译使用 x86 SSE 的现有代码库(编译针对 x86 SSE 指令集的 SIMD 代码)。

code

1.编译针对 x86 SSE 指令集的 SIMD 代码

//  将两个数组a和b的元素逐个相乘,并将结果存储在数组c中。函数使用了AVX指令集中的256位浮点数寄存器__m256来进行向量化计算
void multiply(void) {
    unsigned i;
    __m256 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x7))); i+=8) {// 循环的终止条件是i小于数组长度N并且i是8的倍数,这是因为AVX指令集中的256位浮点数寄存器可以同时处理8个单精度浮点数。
        A = _mm256_load_ps(&a[i]);
        B = _mm256_load_ps(&b[i]);
        C = _mm256_mul_ps(A, B);
        _mm256_store_ps(&c[i], C);// 使用_mm256_store_ps函数将寄存器C中的结果存储回数组c
    }
    for(; i<N; i++) {// 如果数组长度N不是8的倍数,那么循环结束后还会有一些元素没有被处理到。因此,函数使用另一个循环来处理剩下的元素。在这个循环中,函数直接将数组a和b中对应位置的元素相乘,并将结果存储在数组c中。
        c[i] = a[i] * b[i];
    }
}
  • 完整代码
// https://juejin.cn/post/7091571543239000078
// emcc main.c -s ALLOW_MEMORY_GROWTH=1 -Os   -msimd128   -msse   -D USE_SSE    -o wasm_sse_os.html
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "sys/time.h"

// simd内置函数 头文件
#include <immintrin.h>



#define N           178257920 // 170M
#define SEED        0x1234


float *a, *b, *c;


#if defined(NORMAL)
// 为3个float数组分配内存,每个数组包含 170 * 1024 * 1024 个元素
void gen_data(void) {
    unsigned i;
    a = (float*) malloc(N*sizeof(float));
    b = (float*) malloc(N*sizeof(float));
    c = (float*) malloc(N*sizeof(float));
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    free(a);
    free(b);
    free(c);
}

void multiply(void) {
    unsigned i;
    for(i=0; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

#elif defined(USE_SSE)

void gen_data(void) {
    unsigned i;
    a = (float*) _mm_malloc(N*sizeof(float), 16);
    b = (float*) _mm_malloc(N*sizeof(float), 16);
    c = (float*) _mm_malloc(N*sizeof(float), 16);
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    _mm_free(a);
    _mm_free(b);
    _mm_free(c);
}

void multiply(void) {
    unsigned i;
    __m128 A, B, C; // 向量类型 __m128 = 4xfloat

    for(i=0; i<(N & ((~(unsigned)0x3))); i+=4) {
        A = _mm_load_ps(&a[i]);
        B = _mm_load_ps(&b[i]);
        C = _mm_mul_ps(A, B);
        _mm_store_ps(&c[i], C);
    }
    for(; i<N; i++) {
        c[i] = a[i] * b[i];
    }
}

#elif defined(USE_AVX)
void gen_data(void) {
    unsigned i;
    a = (float*) _mm_malloc(N*sizeof(float), 32);
    b = (float*) _mm_malloc(N*sizeof(float), 32);
    c = (float*) _mm_malloc(N*sizeof(float), 32);
    
    srand(SEED);
    for(i=0; i<N; i++) {
        a[i] = b[i] = (float)(rand() % N);
    }
}

void free_data(void) {
    _mm_free(a);
    _mm_free(b);
    _mm_free(c);
}

//  将两个数组a和b的元素逐个相乘,并将结果存储在数组c中。函数使用了AVX指令集中的256位浮点数寄存器__m256来进行向量化计算
void multiply(void) {
    unsigned i;
    __m256 A, B, C;

    for(i=0; i<(N & ((~(unsigned)0x7))); i+=8) {// 循环的终止条件是i小于数组长度N并且i是8的倍数,这是因为AVX指令集中的256位浮点数寄存器可以同时处理8个单精度浮点数。
        A = _mm256_load_ps(&a[i]);
        B = _mm256_load_ps(&b[i]);
        C = _mm256_mul_ps(A, B);
        _mm256_store_ps(&c[i], C);// 使用_mm256_store_ps函数将寄存器C中的结果存储回数组c
    }
    for(; i<N; i++) {// 如果数组长度N不是8的倍数,那么循环结束后还会有一些元素没有被处理到。因此,函数使用另一个循环来处理剩下的元素。在这个循环中,函数直接将数组a和b中对应位置的元素相乘,并将结果存储在数组c中。
        c[i] = a[i] * b[i];
    }
}
#endif


void print_data(void) {
    printf("%f, %f, %f, %f\n", c[0], c[1], c[N-2], c[N-1]);
}

gettimeofday();


int main(void) {
    double start=0.0, stop=0.0, msecs;
    struct timeval before, after;
    printf("gen data start... \n");
    gen_data(); 
    printf("gen data end... \n");

    gettimeofday(&before, NULL);
    multiply();
    gettimeofday(&after, NULL);

    msecs = (after.tv_sec - before.tv_sec)*1000.0 + (after.tv_usec - before.tv_usec)/1000.0;
    print_data();
    printf("Execution time = %2.3lf ms\n", msecs);

    free_data();
    return 0;
}

2.使用<wasm_simd128.h>方式

  • https://jeromewu.github.io/improving-performance-using-webassembly-simd-intrinsics/
#include<stdio.h>
#include<time.h>
#include <wasm_simd128.h>

void multiply_mats(int* out, int* in_a, int* in_b, int n) {
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			out[i*n+j] = 0;
      int sum_arr[] = {0, 0, 0, 0};
      v128_t sum = wasm_v128_load(sum_arr);
			for (int k = 0; k < n; k+=4) {
				v128_t a = wasm_v128_load(&in_a[i*n+k]);
				v128_t b = wasm_v128_load(&in_b[j*n+k]);
				v128_t prod = wasm_i32x4_mul(a, b);
        sum = wasm_i32x4_add(sum, prod);
			}
      v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      out[i*n+j] += wasm_i32x4_extract_lane(sum_one, 0);
		}
	}
}
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js
new.cpp:10:20: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   10 |       v128_t sum = wasm_v128_load(sum_arr);
      |                    ^
new.cpp:12:16: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   12 |                                 v128_t a = wasm_v128_load(&in_a[i*n+k]);
      |                                            ^
new.cpp:13:16: error: always_inline function 'wasm_v128_load' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   13 |                                 v128_t b = wasm_v128_load(&in_b[j*n+k]);
      |                                            ^
new.cpp:14:19: error: always_inline function 'wasm_i32x4_mul' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   14 |                                 v128_t prod = wasm_i32x4_mul(a, b);
      |                                               ^
new.cpp:15:15: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   15 |         sum = wasm_i32x4_add(sum, prod);
      |               ^
new.cpp:17:44: error: '__builtin_wasm_shuffle_i8x16' needs target feature simd128
   17 |       v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      |                                            ^
/home/pdd/Downloads/emsdk/upstream/lib/clang/17/include/wasm_simd128.h:1445:12: note: expanded from macro 'wasm_i32x4_shuffle'
 1445 |   ((v128_t)__builtin_wasm_shuffle_i8x16(                                       \
      |            ^
new.cpp:17:24: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   17 |       v128_t sum_duo = wasm_i32x4_add(sum, wasm_i32x4_shuffle(sum, sum, 2, 3, 0, 0));
      |                        ^
new.cpp:18:48: error: '__builtin_wasm_shuffle_i8x16' needs target feature simd128
   18 |       v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      |                                                ^
/home/pdd/Downloads/emsdk/upstream/lib/clang/17/include/wasm_simd128.h:1445:12: note: expanded from macro 'wasm_i32x4_shuffle'
 1445 |   ((v128_t)__builtin_wasm_shuffle_i8x16(                                       \
      |            ^
new.cpp:18:24: error: always_inline function 'wasm_i32x4_add' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   18 |       v128_t sum_one = wasm_i32x4_add(sum_duo, wasm_i32x4_shuffle(sum_duo, sum_duo, 1, 0, 0, 0));
      |                        ^
new.cpp:19:21: error: always_inline function 'wasm_i32x4_extract_lane' requires target feature 'simd128', but would be inlined into function 'multiply_mats' that is compiled without support for 'simd128'
   19 |       out[i*n+j] += wasm_i32x4_extract_lane(sum_one, 0);
      |                     ^
10 errors generated.
emcc: error: '/home/pdd/Downloads/emsdk/upstream/bin/clang++ -target wasm32-unknown-emscripten -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -DEMSCRIPTEN --sysroot=/home/pdd/Downloads/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat new.cpp -c -o /tmp/emscripten_temp_28ovmxxx/new_0.o' failed (returned 1)
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js -msimd128
$:~/ggml/ggml/examples/mnist/web$ node new.js
multiply matrixs: 3.378s
$:~/ggml/ggml/examples/mnist/web$ emcc new.cpp -o new.js -O3 -msimd128
cache:INFO: generating system asset: symbol_lists/e0d283f526f4ef916e7ac46fde880e1f497e1610.json... (this will be cached in "/home/pdd/Downloads/emsdk/upstream/emscripten/cache/symbol_lists/e0d283f526f4ef916e7ac46fde880e1f497e1610.json" for subsequent builds)
cache:INFO:  - ok
$:~/ggml/ggml/examples/mnist/web$ node new.js
multiply matrixs: 0.338s

限制和行为差异

  • Emscripten 不支持 x86 或任何其他本机内联 SIMD 程序集或构建 .s 程序集文件,因此应编写所有代码以使用 SIMD 内部函数或编译器向量扩展。

  • WebAssembly SIMD 无法控制管理浮点舍入模式或处理非正规数。

  • 缓存行预取指令不可用,对这些函数的调用将编译,但被视为无操作。

  • 非对称内存栅栏操作不可用,但在启用 SharedArrayBuffer (-pthread) 时将实现为完全同步内存栅栏,或者在未启用多线程(默认)时实现为无操作。

CG

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值