禁用优化后,演示代码未能显示出 4 倍快的 SIMD 速度

2023-11-30

我试图了解使用 SIMD 矢量化的好处,并编写了一个简单的演示代码,以了解利用矢量化 (SIMD) 的算法相对于其他算法的速度增益。这是2种算法:

Alg_A - 无矢量支持:

#include <stdio.h>

#define SIZE 1000000000

int main() {
    printf("Algorithm with NO vector support\n");

    int a[] = {1, 2, 3, 4};
    int b[] = {5, 6, 7, 8};
    int i = 0;

    printf("Running loop %d times\n", SIZE);
    for (; i < SIZE; i++) {
        a[0] = a[0] + b[0];
        a[1] = a[1] + b[1];
        a[2] = a[2] + b[2];
        a[3] = a[3] + b[3];
    }

    printf("A: [%d %d %d %d]\n", a[0], a[1], a[2], a[3]);
}

Alg_B - 带矢量支持:

#include <stdio.h>

#define SIZE 1000000000

typedef int v4_i __attribute__ ((vector_size(16)));
union Vec4 {
    v4_i v;
    int i[4];
};

int main() {
    printf("Algorithm with vector support\n\n");

    union Vec4 a, b;
    a.i[0] = 1, a.i[1] = 2, a.i[2] = 3, a.i[3] = 4;
    b.i[0] = 5, b.i[1] = 5, b.i[2] = 7, b.i[3] = 8;
    int i = 0;
    printf("Running loop %d times\n", SIZE);
    for (; i < SIZE; i++) {
        a.v = a.v + b.v;
    }

    printf("A: [%d %d %d %d]\n", a.i[0], a.i[1], a.i[2], a.i[3]);
}

编译过程如下:

Alg_A :

gcc -ggdb -mno-sse -mno-sse2 -mno-sse3 -mno-sse4 -mno-sse4.1 -mno-sse4.2 -c non_vector_support.c
gcc non_vector_support.o -o non_vector_support

Alg_B :

gcc -ggdb -c vector_support.c
gcc vector_support.o -o vector_support

查看两种算法生成的代码,我可以看到编译没有执行任何诸如“自动矢量化”之类的技巧(例如看不到xmm寄存器):

Alg_A :

    for (; i < SIZE; i++) {
  74:   eb 30                   jmp    a6 <main+0xa6>
        a[0] = a[0] + b[0];
  76:   8b 55 d8                mov    -0x28(%rbp),%edx
  79:   8b 45 e8                mov    -0x18(%rbp),%eax
  7c:   01 d0                   add    %edx,%eax
  7e:   89 45 d8                mov    %eax,-0x28(%rbp)
        a[1] = a[1] + b[1];
  81:   8b 55 dc                mov    -0x24(%rbp),%edx
  84:   8b 45 ec                mov    -0x14(%rbp),%eax
  87:   01 d0                   add    %edx,%eax
  89:   89 45 dc                mov    %eax,-0x24(%rbp)
        a[2] = a[2] + b[2];
  8c:   8b 55 e0                mov    -0x20(%rbp),%edx
  8f:   8b 45 f0                mov    -0x10(%rbp),%eax
  92:   01 d0                   add    %edx,%eax
  94:   89 45 e0                mov    %eax,-0x20(%rbp)
        a[3] = a[3] + b[3];
  97:   8b 55 e4                mov    -0x1c(%rbp),%edx
  9a:   8b 45 f4                mov    -0xc(%rbp),%eax
  9d:   01 d0                   add    %edx,%eax
  9f:   89 45 e4                mov    %eax,-0x1c(%rbp)
    int a[] = {1, 2, 3, 4};
    int b[] = {5, 6, 7, 8};
    int i = 0;

    printf("Running loop %d times\n", SIZE);
    for (; i < SIZE; i++) {
  a2:   83 45 d4 01             addl   $0x1,-0x2c(%rbp)
  a6:   81 7d d4 ff c9 9a 3b    cmpl   $0x3b9ac9ff,-0x2c(%rbp)
  ad:   7e c7                   jle    76 <main+0x76>
        a[1] = a[1] + b[1];
        a[2] = a[2] + b[2];
        a[3] = a[3] + b[3];
    }

    printf("A: [%d %d %d %d]\n", a[0], a[1], a[2], a[3]);

Alg_B :

    for (; i < SIZE; i++) {
  74:   eb 16                   jmp    8c <main+0x8c>
        a.v = a.v + b.v;
  76:   66 0f 6f 4d d0          movdqa -0x30(%rbp),%xmm1
  7b:   66 0f 6f 45 e0          movdqa -0x20(%rbp),%xmm0
  80:   66 0f fe c1             paddd  %xmm1,%xmm0
  84:   0f 29 45 d0             movaps %xmm0,-0x30(%rbp)
    union Vec4 a, b;
    a.i[0] = 1, a.i[1] = 2, a.i[2] = 3, a.i[3] = 4;
    b.i[0] = 5, b.i[1] = 5, b.i[2] = 7, b.i[3] = 8;
    int i = 0;
    printf("Running loop %d times\n", SIZE);
    for (; i < SIZE; i++) {
  88:   83 45 cc 01             addl   $0x1,-0x34(%rbp)
  8c:   81 7d cc ff c9 9a 3b    cmpl   $0x3b9ac9ff,-0x34(%rbp)
  93:   7e e1                   jle    76 <main+0x76>
        a.v = a.v + b.v;
    }

    printf("A: [%d %d %d %d]\n", a.i[0], a.i[1], a.i[2], a.i[3]);

当我运行程序时,我希望看到速度提高 4 倍,但是,对于这种大小的数据,增益似乎平均 =~ 1 秒,如果将 SIZE 增加到大约 8000000000,则增益 = 〜5秒。这是上面代码中的值的时间:

Alg_A :

Algorithm with NO vector support
Running loop 1000000000 times
A: [705032705 1705032706 -1589934589 -589934588]

real    0m3.279s
user    0m3.282s
sys     0m0.000s

Alg_B :

具有向量支持的算法

Running loop 1000000000 times
A: [705032705 705032706 -1589934589 -589934588]

real    0m2.609s
user    0m2.607s
sys     0m0.004s

计算与循环相关的开销。我针对给定的 SIZE 运行了一个空循环,并获得了 =~ 2.2 秒的平均时间。这使我的平均速度提高了约 2.5 倍。

我是否遗漏了代码或编译行中的某些内容?或者,这是否被认为是正确的,在这种情况下,有人会知道如果我在每次迭代中利用 4 个数据点和 1 条指令,为什么速度没有提高 4 倍?

提前致谢。


我在下面整理了一些示例代码,以说明您如何看待 SIMD 相对于标量代码的优势。示例代码有点做作,但要注意的要点是循环中需要有足够的算术运算来减轻加载/存储延迟和循环开销 - 正如您最初的实验一样,单个添加操作是不够的。

此示例将 32 位 int 数据的吞吐量提高了约 4 倍。 SIMD 循环有两种版本:一种是不展开的简单循环,另一种是进行 2 次展开的替代循环。正如预期的那样,展开的循环要快一些。

#include <assert.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/time.h>   // gettimeofday
#include <smmintrin.h>  // SSE 4.1

static void foo_scalar(uint32_t *a, const uint32_t *b, const uint32_t *c, size_t n)
{
    for (size_t i = 0; i < n; ++i)
    {
        a[i] = (b[i] + c[i] + 1) / 2;
    }
}

static void foo_simd(uint32_t *a, const uint32_t *b, const uint32_t *c, size_t n)
{
    size_t i;

#ifndef UNROLL
    for (i = 0; i <= n - 4; i += 4)
    {
        __m128i vb = _mm_loadu_si128((__m128i *)&b[i]);
        __m128i vc = _mm_loadu_si128((__m128i *)&c[i]);
        __m128i v = _mm_add_epi32(vb, vc);
        v = _mm_add_epi32(v, _mm_set1_epi32(1));
        v = _mm_srli_epi32(v, 1);
        _mm_storeu_si128((__m128i *)&a[i], v);
    }
#else
    for (i = 0; i <= n - 8; i += 8)
    {
        __m128i vb0 = _mm_loadu_si128((__m128i *)&b[i]);
        __m128i vb1 = _mm_loadu_si128((__m128i *)&b[i + 4]);
        __m128i vc0 = _mm_loadu_si128((__m128i *)&c[i]);
        __m128i vc1 = _mm_loadu_si128((__m128i *)&c[i + 4]);
        __m128i v0 = _mm_add_epi32(vb0, vc0);
        __m128i v1 = _mm_add_epi32(vb1, vc1);
        v0 = _mm_add_epi32(v0, _mm_set1_epi32(1));
        v1 = _mm_add_epi32(v1, _mm_set1_epi32(1));
        v0 = _mm_srli_epi32(v0, 1);
        v1 = _mm_srli_epi32(v1, 1);
        _mm_storeu_si128((__m128i *)&a[i], v0);
        _mm_storeu_si128((__m128i *)&a[i + 4], v1);
    }
#endif
    foo_scalar(&a[i], &b[i], &c[i], n - i);
}

int main(int argc, char *argv[])
{
    const size_t kLoops = 100000;
    size_t n = 2 * 1024;
    struct timeval t0, t1;
    double t_scalar_ms, t_simd_ms;

    if (argc > 1)
    {
        n = atoi(argv[1]);
    }

    printf("kLoops = %zu, n = %zu\n", kLoops, n);

    uint32_t * a_scalar = malloc(n * sizeof(uint32_t));
    uint32_t * a_simd = malloc(n * sizeof(uint32_t));
    uint32_t * b = malloc(n * sizeof(uint32_t));
    uint32_t * c = malloc(n * sizeof(uint32_t));

    for (size_t i = 0; i < n; ++i)
    {
        a_scalar[i] = a_simd[i] = 0;
        b[i] = rand();
        c[i] = rand();
    }

    gettimeofday(&t0, NULL);
    for (size_t k = 0; k < kLoops; ++k)
    {
        foo_scalar(a_scalar, b, c, n);
    }
    gettimeofday(&t1, NULL);
    t_scalar_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;

    gettimeofday(&t0, NULL);
    for (size_t k = 0; k < kLoops; ++k)
    {
        foo_simd(a_simd, b, c, n);
    }
    gettimeofday(&t1, NULL);
    t_simd_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;

    int64_t sum_scalar = 0, sum_simd = 0;
    for (size_t i = 0; i < n; ++i)
    {
        sum_scalar += a_scalar[i];
        sum_simd += a_simd[i];
    }
    assert(sum_scalar == sum_simd);

    printf("t_scalar = %8g ms = %8g ns / point\n", t_scalar_ms, t_scalar_ms / (kLoops * n) * 1e6);
    printf("t_simd   = %8g ms = %8g ns / point\n", t_simd_ms, t_simd_ms / (kLoops * n) * 1e6);
    printf("Speed-up = %2.1fx\n",  t_scalar_ms / t_simd_ms);

    return 0;
}

编译并运行(无 SIMD 循环展开):

$ gcc-4.8 -fno-tree-vectorize -std=gnu99 -Wall gros_lalo.c -O3 -msse4.1 && ./a.out
kLoops = 100000, n = 2048
t_scalar =  122.668 ms = 0.598965 ns / point
t_simd   =   33.785 ms = 0.164966 ns / point
Speed-up = 3.6x

编译并运行(2x SIMD 循环展开):

$ gcc-4.8 -fno-tree-vectorize -std=gnu99 -Wall gros_lalo.c -O3 -msse4.1 -DUNROLL && ./a.out
kLoops = 100000, n = 2048
t_scalar =  121.897 ms =   0.5952 ns / point
t_simd   =    29.07 ms = 0.141943 ns / point
Speed-up = 4.2x

查看生成的代码很有趣:

Scalar:

    xorl    %ecx, %ecx
    .align 4
L10:
    movl    0(%rbp,%rcx,4), %esi
    addl    (%rbx,%rcx,4), %esi
    addl    $1, %esi
    shrl    %esi
    movl    %esi, (%r15,%rcx,4)
    addq    $1, %rcx
    cmpq    %r12, %rcx
    jne L10

SIMD(不展开):

    xorl    %ecx, %ecx
    xorl    %eax, %eax
    .align 4
L18:
    movdqu  0(%rbp,%rcx), %xmm2
    addq    $4, %rax
    movdqu  (%rbx,%rcx), %xmm1
    paddd   %xmm2, %xmm1
    paddd   %xmm3, %xmm1
    psrld   $1, %xmm1
    movdqu  %xmm1, (%r14,%rcx)
    addq    $16, %rcx
    cmpq    %r9, %rax
    jbe L18

SIMD(2x 展开):

    xorl    %edx, %edx
    xorl    %ecx, %ecx
    .align 4
L18:
    movdqu  0(%rbp,%rdx), %xmm5
    addq    $8, %rcx
    movdqu  (%r11,%rdx), %xmm4
    movdqu  (%rbx,%rdx), %xmm2
    movdqu  (%r10,%rdx), %xmm1
    paddd   %xmm5, %xmm2
    paddd   %xmm4, %xmm1
    paddd   %xmm3, %xmm2
    paddd   %xmm3, %xmm1
    psrld   $1, %xmm2
    psrld   $1, %xmm1
    movdqu  %xmm2, 0(%r13,%rdx)
    movdqu  %xmm1, (%rax,%rdx)
    addq    $32, %rdx
    cmpq    %r15, %rcx
    jbe L18

请注意,前两个循环中的指令数量相似,但 SIMD 循环每次迭代当然处理四个元素,而标量循环每次迭代仅处理一个元素。对于第三个展开循环,我们有更多指令,但每次迭代处理 8 个元素 - 请注意,相对于没有循环展开的 SIMD 循环,循环管理指令的比例已减少。

计时数据是使用 2.6 GHz Core i7 Haswell CPU 在 Mac OS X 10.10 上使用 gcc 4.8 收集的。然而,在任何当前合理的 x86 CPU 上,性能结果应该相似。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

禁用优化后,演示代码未能显示出 4 倍快的 SIMD 速度 的相关文章

随机推荐

  • 如何移动正交图中的点?

    我正在尝试在迈克 博斯托克创建的以下地图上的某些地理位置添加红点 https bl ocks org mbostock 3795040 我的点会显示 但不会随地图移动 如何编辑代码以使点随地图移动 谢谢 add circles to svg
  • 如何使用javascript从mysql数据库获取数据?

    如果可能的话 如何使用 javascript 从 mysql 数据库获取 并发布 数据 我知道我可以使用 php 和其他语言 但我只需要知道这是否可以用 javascript 实现 提前致谢 这对于 Javascript 来说是不可能的 我
  • 如何响应 UNUserNotification 上的点击?

    我正在使用新的UNUserNotificationiOS 10 中的框架 我可以看到如何添加操作按钮 但是当用户点击通知本身时我如何响应 就我而言 它将是带有一些文本的图像 默认行为是应用程序打开 我可以使用自定义代码来检测我的应用程序是否
  • 如何在pyodbc中使用executemany运行多个SELECT查询

    我使用 PYODBC 根据 pandas 数据帧列的值多次查询 SQL DB 如下所示为值列表 因为我使用 ToList 函数将该列转换为列表 the connection string cnxn pyodbc connect driver
  • 扫描随机数量的浮点数,直到 C 中出现新行

    我正在尝试从包含以下文本的文件中读取 502 601 596 465 464597 599 600 598602 591 596 601588 565 548 260 62 61 595583 595 61 558 561237 241 4
  • 分析 C# 中的方法以了解其运行时间

    我需要获取计时报告以了解在类中运行 C 方法需要多长时间 我考虑使用profiler要做到这一点 输入是类中方法的名称 输出是 什么方法 类调用这个方法 运行该方法的时间量 有哪些工具 商业产品可用于 Visual Studio 2010
  • 在 TypoScript 中获取 FlexForm 配置

    我需要从 pi flexform 获取 typescript 中的 page headerData 如何实现我的要求 page PAGE page headerData 10 TEXT 10 value 我不太确定你真正需要什么 我是gue
  • SlidingDrawer 动画速度

    我是 Android 编程和堆栈溢出的新手 我需要减慢应用程序中 SlidingDrawer 的动画速度 我已经像这样子类化了 SlidingDrawer import android content Context import andr
  • 最大化两个数组元素的乘积之和的算法

    竞赛中有一个问题需要计算仅包含数学和生物科目的班级的表现 所以 没有 数学学生 n 没有 的生物学生 每个学生都有一个单独的分数 数学学生和生物学生的分数分别存储在数组 mathScore 和 bioScore 中 全班成绩计算如下 mat
  • 从存储过程填充 DataGridView

    我使用 SQL Server 2008 创建了一个名为 MyStoreProc 的存储过程 它在管理工具中运行良好 在 VB Net 2008 中 我创建了一个新的数据集和一个新的 TableAdaptor 在此表适配器中 我创建了一个名为
  • 如何从树状数组创建 ul - li 菜单?

    我有一个数组title and children index title始终不为空 children是一个数组 空或非空 Any children have title and children等等 myArray 0 gt title g
  • JTable右键复制/粘贴菜单一键复制单元格数据

    我创建了我的JPopupMenu 它出现在我的JTable当我右键单击一个单元格时 但是 我无法复制单元格中的数据 除非我首先双击然后突出显示数据 然后右键单击当前单元格以外的任何位置以显示弹出菜单和复制选项 我想复制单元格中的数据 而不必
  • Perl - 子例程“Hash::Merge::merge”的深度递归

    下列的this问题 我在那里使用了答案 也发布在这里 现在我失败了 我知道失败可能来自于 return bless self gt merge left right class left 但我不明白可能是什么问题 My code usr b
  • 使用 Windows 服务和 SQL Server 在 OneWay WCF 消息中排队

    我需要为 WCF 服务请求实现一个排队机制 该服务将由客户端以单向方式调用 这些请求消息应存储在 SQL Server 数据库中 并且 Windows 服务对消息进行排队 处理请求的时间是可配置的 如果处理消息时发生错误 则需要重试最多10
  • MySQL 5.7 错误(1093:您无法在 FROM 子句中指定目标表 ___ 进行更新) - 通常的解决方案不起作用

    我有一个表 员工 我试图将一些属性 例如薪水 设置为与表中其他值相同的值 我对这个错误的理解是 可以通过以下解决方法来避免它 使用临时表 UPDATE employees SET salary SELECT salary FROM SELE
  • 当使用非虚拟析构函数“删除”基类时,Clang 和 GCC 会做什么?

    已经有一个问题询问 现实世界 的行为delete指向缺少虚拟析构函数的基类的指针 但问题仅限于非常有限的情况 派生类没有具有非平凡析构函数的成员 并且接受的答案只是说没有办法知道不检查每个编译器的行为 但这实际上并不是很有帮助 知道每个编译
  • authorize.net json返回额外字符

    我有这个代码 ch curl init curl setopt ch CURLOPT URL url curl setopt ch CURLOPT RETURNTRANSFER 1 curl setopt ch CURLOPT HTTPHE
  • Laravel 5 如何在保存时验证每个活动下的唯一客户名称

    我有三个模型 活动模型 客户模型和客户项目模型 如何在商店功能中进行验证检查 使每个活动中的客户名称应该是唯一的 以下是每个迁移文件 活动模型 public function up Schema create activities func
  • Angular Material 6 中用于自动完成的无限滚动

    我正在尝试在 Angular Material 6 中实现自动完成的无限滚动 我的场景很简单 我有一个启用了自动完成功能的输入字段 当用户键入时 我将使用输入字段中的文本进行 HTTP 调用 以将结果显示为建议 但我只想显示 25 条建议
  • 禁用优化后,演示代码未能显示出 4 倍快的 SIMD 速度

    我试图了解使用 SIMD 矢量化的好处 并编写了一个简单的演示代码 以了解利用矢量化 SIMD 的算法相对于其他算法的速度增益 这是2种算法 Alg A 无矢量支持 include