向量相加其二(C串行、OpenMP、OpenMP AVX2实现)

摘要

向量相加一中比较了纯python和numpy实现向量相加的速度情况
在本文中我们使用C语言来讨论向量相加的加速,因为C语言是公认的执行效率较高的高级语言
一般我们个人学习生活中编程思想都是在单个CPU逻辑核上的,现在大家的个人笔记本基本都是4核8核10核甚至80核128核,超算的核数更是成千上万,这里的核就是指的逻辑核
如果我们将单核上的任务分给多核,将会得到显著的性能提升
本文实现的加速方法:

  1. 普通C串行程序
  2. 使用OpenMP并行化向量计算
  3. 使用OpenMP和AVX2指令集并行化计算

测试机配置

由于上一台测试机处理器不支持AVX2指令集,因此换用我的游戏本

(a)	CPU
厂家:Intel
型号:Intel®Core™ i7-7700HQ CPU
核数:8
频率:2.8GHz
指令集:支持AVX2,不支持AVX512

(b)	GPU0
厂家:Intel
型号:Intel®UHD Graphics 630
显存容量:N/A
显存频率:350MHz
显存带宽:19.2GB/s

GPU1
厂家:NVIDIA
型号:NVIDIA GeForce GTX 1050Ti
显存容量:4GB
显存频率:1752MHz
显存带宽:112.1GB/s

源代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <omp.h>
#include <immintrin.h>

int main(int argc, char **argv)
{
    int n = atoi(argv[1]);
    int repeat = 10;

    // create vectors
    int *a = (int *)malloc(sizeof(int) * n);
    int *b = (int *)malloc(sizeof(int) * n);
    int *c = (int *)malloc(sizeof(int) * n);

    for (int i = 0; i < n; i++)
    {
        a[i] = 1;
        b[i] = 2;
    }

    struct timeval t1, t2;

    // serial version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        for (int i = 0; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    double time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C serial takes %f sec\n", time_serial);

    // OpenMP parallel version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        #pragma omp parallel for
        for (int i = 0; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C OpenMP takes %f sec\n", time_serial);

    // OpenMP AVX2 parallel version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        int loop = n / 8;
        for (int tid = 0; tid < loop; tid++)
        {
            __m256i aavx2 = _mm256_loadu_si256((__m256i*)(&a[tid * 8]));
            __m256i bavx2 = _mm256_loadu_si256((__m256i*)(&b[tid * 8]));
            __m256i cavx2 = _mm256_add_epi32(aavx2, bavx2);
            _mm256_storeu_si256((__m256i*)(&c[tid * 8]), cavx2);
        }
        #pragma omp parallel for
        for (int i = loop * 8; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C OpenMP AVX2 takes %f sec\n", time_serial);

    free(a);
    free(b);
    free(c);
}

说明

1.C串行版和OpenMP版可以用idle环境编译,AVX2普通idle可能识别不出

C语言并行计算的加速基本都是对内存直接进行操作,还是放弃idle直接将gcc编译器设置成环境变量然后用cmd编译吧,越早习惯不用idle越好

打开cmd输入gcc -v出现如下字样就说明设置环境变量成功

C:\Users\admin>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=d:/dev-cpp/mingw32/bin/../libexec/gcc/mingw32/4.7.2/lto-wrapper.exe
Target: mingw32
Configured with: ../gcc-4.7.2/configure --enable-languages=c,c++,ada,fortran,objc,obj-c++ --disable-sjlj-exceptions --with-dwarf2 --enable-shared --enable-libgomp --disable-win32-registry --enable-libstdcxx-debug --disable-build-poststage1-with-cxx --enable-version-specific-runtime-libs --build=mingw32 --prefix=/mingw
Thread model: win32
gcc version 4.7.2 (GCC)

2.sys/time.h这个头文件idle环境也可能识别不了

sys/time.h原本是Linux系统下定义的获取时间等等一系列,Windows下普通idle可以识别time.h,不过没关系,直接打开cmd用gcc编译器编译windows下也能识别和运行。非要用time.h的话计时方法也要更改。

运行结果及分析

C:\Users\admin>cd C:\Users\admin\Desktop\

C:\Users\admin\Desktop>gcc -O3 -fopenmp -mavx2 0.c -o 0 -std=c99

C:\Users\admin\Desktop>0.exe 1000000
C serial takes 0.000999 sec
C OpenMP takes 0.001000 sec
C OpenMP AVX2 takes 0.001000 sec

C:\Users\admin\Desktop>0.exe 10000000
C serial takes 0.007998 sec
C OpenMP takes 0.007000 sec
C OpenMP AVX2 takes 0.007000 sec

C:\Users\admin\Desktop>0.exe 100000000
C serial takes 0.083790 sec
C OpenMP takes 0.062064 sec
C OpenMP AVX2 takes 0.069361 sec



C:\Users\admin\Desktop>gcc -fopenmp -mavx2 0.c -o 0 -std=c99

C:\Users\admin\Desktop>0.exe 1000000
C serial takes 0.003001 sec
C OpenMP takes 0.000998 sec
C OpenMP AVX2 takes 0.001002 sec

C:\Users\admin\Desktop>0.exe 10000000
C serial takes 0.027000 sec
C OpenMP takes 0.007997 sec
C OpenMP AVX2 takes 0.009002 sec

C:\Users\admin\Desktop>0.exe 100000000
C serial takes 0.276776 sec
C OpenMP takes 0.068133 sec
C OpenMP AVX2 takes 0.084478 sec

-O3是编译器3级优化参数,-On这个n越大优化级别越高,不加就是对代码不进行编译器优化
-fopenmp对应omp.h头文件,加这些才可以用OpenMP
-mavx2对应immintrin.h头文件,加这些才可以用AVX2指令

我们可以看到C语言的速度远胜python,而加了O3优化的C串行程序在10的6、7、8次方数量级的向量相加情况下紧咬OpenMP和OPenMP AVX2加速的程序,这就可以看出编译器在我们写代码生活中做出的巨大贡献。
一旦将编译优化参数去掉,后两个版本的代码优势十分明显。

AVX2指令优化类似汇编语言,上面的程序中我们使用AVX2C语言指令从数组a,b的内存中一次分别各取出8个数,相加存到另一个_m256i变量中,再存回到数组c当中,再使用OpenMP并行将为8的倍数个计算好的结果合并,得到最终结果。

AVX2指令集一次可以处理32个二进制位的数据,也就是8个int型或者float型,或者4个double型数据。

我们可以发现,向量计算也好、矩阵计算也好,并行化之后计算之所以快,就是得益于这种分块(block)的思想。但是上面结果OpenMP AVX2版的并不比OpenMP的快多少,这是由于存存取取的操作占用了时间,AVX2无法直接处理C语言中的变量。这和GPU加速是一样的。AVX2加速和GPU加速都要付出这样的时间代价。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章