这篇客串帖子的作者是 来自英特尔公司的董俊峰、摩根和李天 .
去年我们引进了英特尔® 高级矢量扩展512(Intel® AVX-512)支持Microsoft*Visual Studio*2017至 这个VC++博客 发布。在这篇后续文章中,我们将介绍一些示例,让您了解英特尔® AVX-512提供性能优势。这些例子包括计算数组的平均值、矩阵向量乘法和Mandelbrot集的计算。Microsoft Visual Studio 2017 15.5或更高版本(我们建议 最新的 )是编译这些演示所必需的,并且是一台带有英特尔的计算机® 支持英特尔® 运行它们需要AVX-512指令集。看看我们的 方舟网站 获取处理器列表。
下面是我们用于编译和运行这些示例的硬件和软件配置:
- 处理器:英特尔® 核心™ i9 7980XE CPU@2.60GHz,18核/36线程
- 内存:64GB
- 操作系统:Windows 10企业版10.0.17134.112
- Windows中的电源选项:平衡
- Microsoft Visual Studio 2017版本15.8.3
注:任何现代处理器的性能都取决于许多因素,包括内存和存储器的数量和类型、图形和显示系统、特定的处理器配置以及程序如何有效地利用这一切。因此,我们得到的结果可能不完全匹配其他配置的结果。
让我们通过一个简单的例子来了解如何使用Intel编写一些代码® AVX-512内部函数。顾名思义,英特尔® 高级向量扩展(Intel® AVX)和英特尔® AVX-512指令旨在使用 计算向量 ,其中包含多个相同数据类型的元素。我们将使用这些AVX和AVX-512内在函数来加速计算浮点数数组的平均值。
首先,我们从一个函数开始,该函数使用标量(非向量)运算计算数组“a”的平均值:
static const int length = 1024*8; static float a[length]; float scalarAverage() { float sum = 0.0; for (uint32_t j = 0; j < _countof(a); ++j) { sum += a[j]; } return sum / _countof(a);
此例程对数组的所有元素求和,然后除以元素数。为了“矢量化”这个计算,我们将数组分解成可以同时计算的元素或“矢量”组。英特尔公司® AVX我们能装8个 浮动 每个向量中的元素,因为每个浮点是32位,向量是256位,所以256/32=8 浮动 元素。我们把所有的群求和成一个部分和向量,然后把这个向量的元素相加得到最终的和。为简单起见,我们假设在下面所示的示例代码中,AVX和AVX-512的元素数分别是8或16的倍数。如果总元素的数量不能很好地适应这些向量,则必须编写一个特殊的案例来处理剩余的元素。
新功能如下:
float avxAverage () { __m256 sumx8 = _mm256_setzero_ps(); for (uint32_t j = 0; j < _countof(a); j = j + 8) { sumx8 = _mm256_add_ps(sumx8, _mm256_loadu_ps(&(a[j]))); } float sum = sumx8.m256_f32[0] + sumx8.m256_f32[1] + sumx8.m256_f32[2] + sumx8.m256_f32[3] + sumx8.m256_f32[4] + sumx8.m256_f32[5] + sumx8.m256_f32[6] + sumx8.m256_f32[7]; return sum / _countof(a); }
在这里, _mm256设置零u ps 创建一个具有八个零值的向量,该向量被指定给sumx8。然后,对于数组中的每一组八个相邻元素, _mm256加载u ps 将它们加载到256位向量中 _mm256添加u ps 添加到sumx8中的相应元素,使sumx8成为包含八个小计的向量。最后,将这些小计相加以创建总数。
与标量实现相比,这种单指令多数据(SIMD)实现执行的add指令更少。对于具有 n 元素,将执行标量实现 n 添加指令,但使用Intel® 仅限AVX( n /8+7)需要添加说明。因此,英特尔® AVX可能比标量实现快8倍。
类似地,下面是英特尔® AVX-512版本:
float avx512AverageKernel() { __m512 sumx16 = _mm512_setzero_ps(); for (uint32_t j = 0; j < _countof(a); j = j + 16) { sumx16 = _mm512_add_ps(sumx16, _mm512_loadu_ps(&(a[j]))); } float sum = _mm512_reduce_add_ps (sumx16); return sum / _countof(a);
如您所见,除了计算最终和的方式外,这个版本几乎与前面的函数相同。对于向量中的16个元素,如果我们使用与AVX版本相同的方法,我们需要16个数组引用和15个加法来计算最终的和。幸运的是,AVX-512提供了 _mm512u减少u添加u ps 内部函数生成相同的结果,这使代码更易于阅读。此函数将前八个元素与其余元素相加,然后将该向量的前四个元素与其余元素相加,再加上两个元素,最后用四条加法指令求和,得到总数。使用英特尔® AVX-512查找数组的平均值 n 元素需要执行( n /16+4)加法指令,大约是英特尔所需指令的一半® AVX什么时候 n 它很大。因此,英特尔® AVX-512可能比Intel快2倍® AVX公司。
对于这个例子,我们只使用了一些最基本的英特尔® AVX-512内部函数,但是有数百个向量内部函数可用,它们对128位、256位和512位向量中的不同数据类型的选择执行各种操作,并具有诸如掩蔽和舍入之类的选项。这些函数可能使用来自各种Intel的指令® 指令集扩展,包括Intel® AVX-512。例如,内部函数 _mm512u添加u ps() 使用Intel® AVX-512型 吸血鬼 说明。你可以用 英特尔软件开发人员手册 了解有关英特尔的更多信息® AVX-512指令和 英特尔内部函数指南 寻找特定的内在函数。单击 本质指南 它将扩展到显示更多细节,如概要、描述和操作。
这些函数是使用 免疫素h 标题。微软还提供 内啡肽h 标头,它几乎声明了所有微软Visual C++(MSVC)固有函数,包括来自IMMtIr.h的函数。您可以在源文件中包含这些头文件中的任何一个。
数学向量和矩阵运算涉及许多乘法和加法。例如,这里是矩阵和向量的简单乘法:
一台计算机几乎可以立即完成这个简单的计算,但是将非常大的向量和矩阵相乘可能需要成百上千的乘法和加法。让我们看看如何“矢量化”这些计算,使他们更快。让我们从一个与矩阵相乘的标量函数开始 t1级 带矢量 t2级 :
static float *out; static const int row = 16; static const int col = 4096; static float *scalarMultiply() { for (uint64_t i = 0; i < row; i++) { float sum = 0; for (uint64_t j = 0; j < col; j++) sum = sum + t1[i * col + j] * t2[j]; out[i] = sum; } return out; }
与前面的示例一样,我们通过将每一行分解为向量来“矢量化”此计算。对于AVX-512,因为 浮动 是32位,矢量是512位,矢量可以有512/32=16 浮动 元素。请注意,这是一个 计算的 向量,它不同于 数学的 正在相乘的向量。对于矩阵中的每一行,我们加载16列以及向量中相应的16个元素,将它们相乘,然后将乘积加到16个元素的累加器中。当行完成时,我们可以对累加器元素求和,得到结果的一个元素。请注意,我们可以用英特尔做到这一点® AVX或Intel® 数据流单指令多数据扩展指令集(英特尔® SSE),这些扩展的最大向量大小分别为8(256/32)和4(128/32)个元素。
使用英特尔的乘法程序的一个版本® AVX-512内部函数如下所示:
static float *outx16; static float *avx512Multiply() { for (uint64_t i = 0; i < row; i++) { __m512 sumx16 = _mm512_set1_ps(0.0); for (uint64_t j = 0; j < col; j += 16) { __m512 a = _mm512_loadu_ps(&(t1[i * col + j])); __m512 b = _mm512_loadu_ps(&(t2[j])); sumx16 = _mm512_fmadd_ps(a, b, sumx16); } outx16[i] = _mm512_reduce_add_ps(sum16); } return outx16; }
您可以看到,许多标量表达式已被对向量执行相同操作的内部函数调用所取代。
我们将替换 总和 通过呼叫 _mm512u设置1u ps() 函数,该函数创建一个包含16个零元素的向量并将其赋给 sumx16号 . 在内部循环中,我们加载16个元素 t1级 和 t2级 变成向量变量 一 和 b 分别使用 _mm512u加载u ps() . 这个 _mm512u fmaddu ps() 函数将中每个元素的乘积相加 一 和 b 中相应的元素 sumx16号 .
在内环的末尾,我们有16个部分和 sumx16号 而不是一笔钱。为了计算最终结果,我们必须使用 _mm512u减少u添加u ps () 我们在数组平均值示例中使用的函数。
在这一点上,我们应该注意,这个矢量化版本不计算 确切地 与标量版本相同。如果我们用数学方法来计算 实数 我们添加部分产品的顺序无关紧要,但事实并非如此 浮点 价值观。添加浮点值时,精确结果可能无法表示为浮点值。在这种情况下,结果必须四舍五入到可以表示的两个最接近的值之一。精确结果和可表示结果之间的区别是 舍入误差 .
当计算这样的乘积之和时,计算结果与精确结果的差异是所有舍入误差之和。由于矢量化矩阵乘法以不同于标量形式的顺序添加部分积,因此每次添加的舍入误差也可能不同。此外 _mm512u添加u ps () 函数在将部分积添加到部分和之前不会对其进行舍入,因此只有加法才会添加舍入错误。如果标量计算和矢量计算的舍入误差不同,结果也可能不同。然而,这并不意味着两个版本都是错误的。它只是显示了浮点计算的特性。
这个 曼德布罗特集 是由迭代定义的序列的所有复数z的集合
z(0)=z,z(n+1)=z(n)*z(n)+z,n=0,1,2…
保持有界。这意味着存在一个数字B,使得所有迭代z(n)的绝对值永远不会大于B。Mandelbrot集的计算经常被用来制作彩色的图像 不 在集合中,每种颜色表示需要多少项才能超出界限。它被广泛地用作一个例子来演示向量计算的性能。给出了计算B为2.0的Mandelbrot集的核心代码 在这里 .
static int mandel(float c_re, float c_im, int count) { float z_re = c_re, z_im = c_im; int i; for (i = 0; i < count; ++i) { if (z_re * z_re + z_im * z_im > 4.f) break; float new_re = z_re * z_re - z_im * z_im; float new_im = 2.f * z_re * z_im; z_re = c_re + new_re; z_im = c_im + new_im; } return i; }
当然,不可能计算无穷级数的每一项,因此此函数计算的项数不超过 计数 ,我们假设如果序列没有偏离这个点,它就不会偏离。返回的值表示有多少项没有分叉,通常用于为该点选择颜色。如果函数返回 计数 假设点在Mandelbrot集合中。
我们可以通过用一个向量等价物替换每个标量操作来将其矢量化,类似于我们矢量化矩阵向量乘法的方式。但是下面的源代码行出现了一个复杂的问题:“if(z泷re*z泷re+z泷im*z泷im>4.0f)break;”。如何将条件中断矢量化?
在这种情况下,我们知道一旦序列超出了界限,所有后续项也将超出界限,因此我们可以无条件地继续计算所有元素,直到所有元素都超出界限或达到迭代极限。我们可以通过使用向量比较来屏蔽已超出界限的元素,并使用它来更新其余元素的结果来处理该条件。以下是使用英特尔的一个版本的代码® 高级向量扩展2(Intel® AVX2)功能。
/* AVX2 Implementation */ __m256i avx2Mandel (__m256 c_re8, __m256 c_im8, uint32_t max_iterations) { __m256 z_re8 = c_re8; __m256 z_im8 = c_im8; __m256 four8 = _mm256_set1_ps(4.0f); __m256 two8 = _mm256_set1_ps(2.0f); __m256i result = _mm256_set1_epi32(0); __m256i one8 = _mm256_set1_epi32(1); for (auto i = 0; i < max_iterations; i++) { __m256 z_im8sq = _mm256_mul_ps(z_im8, z_im8); __m256 z_re8sq = _mm256_mul_ps(z_re8, z_re8); __m256 new_im8 = _mm256_mul_ps(z_re8, z_im8); __m256 z_abs8sq = _mm256_add_ps(z_re8sq, z_im8sq); __m256 new_re8 = _mm256_sub_ps(z_re8sq, z_im8sq); __m256 mi8 = _mm256_cmp_ps(z_abs8sq, four8, _CMP_LT_OQ); z_im8 = _mm256_fmadd_ps(two8, new_im8, c_im8); z_re8 = _mm256_add_ps(new_re8, c_re8); int mask = _mm256_movemask_ps(mi8); __m256i masked1 = _mm256_and_si256(_mm256_castps_si256(mi8), one8); if (0 == mask) break; result = _mm256_add_epi32(result, masked1); } return result; }
标量函数返回一个值,该值表示在结果发散之前计算了多少次迭代,因此向量函数必须返回具有这些相同值的向量。我们首先生成一个向量来计算 军情八处 指示哪些元素未超出界限的。这个向量的每个元素要么都是零位(如果测试条件 _凸轮轴位置 不是真的)或所有一位(如果是真的)。如果向量都为零,那么所有的东西都发散了,我们就可以脱离这个循环。否则,向量值将被重新解释为32位整数值的向量 _mm256u铸件u si256 ,然后用32位1的向量屏蔽。这就给每个没有发散的元素留了一个值,而那些发散的元素留了0。剩下的就是把向量加到向量累加器中 结果 .
英特尔® AVX-512版本的这个功能是类似的,有一个显著的区别。这个 _mm256u cmpu ps 函数返回类型为的向量值 __m256型 . 您可能希望使用 _mm512u cmpu ps 返回类型为的向量的函数 __m512型 ,但该函数不存在。相反,我们使用 _mm512 cmp ps屏蔽 返回类型为的值的函数 __mmask16型 . 这是一个16位的值,其中每个位表示向量的一个元素。这些值保存在一组单独的八个寄存器中,用于矢量化条件执行。情报在哪里® AVX2函数必须计算要显式添加到结果的值® AVX-512允许将掩码直接应用于具有 _mm512u掩码u添加u epi32 功能。
/* AVX512 Implementation */ __m512i avx512Mandel(__m512 c_re16, __m512 c_im16, uint32_t max_iterations) { __m512 z_re16 = c_re16; __m512 z_im16 = c_im16; __m512 four16 = _mm512_set1_ps(4.0f); __m512 two16 = _mm512_set1_ps(2.0f); __m512i one16 = _mm512_set1_epi32(1); __m512i result = _mm512_setzero_si512(); for (auto i = 0; i < max_iterations; i++) { __m512 z_im16sq = _mm512_mul_ps(z_im16, z_im16); __m512 z_re16sq = _mm512_mul_ps(z_re16, z_re16); __m512 new_im16 = _mm512_mul_ps(z_re16, z_im16); __m512 z_abs16sq = _mm512_add_ps(z_re16sq, z_im16sq); __m512 new_re16 = _mm512_sub_ps(z_re16sq, z_im16sq); __mmask16 mask = _mm512_cmp_ps_mask(z_abs16sq, four16, _CMP_LT_OQ); z_im16 = _mm512_fmadd_ps(two16, new_im16, c_im16); z_re16 = _mm512_add_ps(new_re16, c_re16); if (0 == mask) break; result = _mm512_mask_add_epi32(result, mask, result, one16); } return result; }
每个向量化的Mandelbrot计算都返回一个向量而不是一个标量,并且每个元素的值与原始标量函数返回的值相同。您可能已经注意到返回值的类型与实参数向量和虚参数向量不同。标量函数的参数是类型 浮动 ,函数返回 无符号整数 . 向量化函数的参数是的向量版本 浮动 ,返回值是一个可以容纳32位无符号整数的向量。如果需要对使用类型的函数进行矢量化 双重的 ,也有用于保存该类型元素的向量类型: __m128d型 , __m256d型 和 __m512d型 . 您可能想知道是否有其他整数类型的向量类型,例如 有符号字符 和 无符号短 . 没有。类型向量 __m128i型 , __m256i型 和 __m512i型 用于所有整数元素,而不考虑大小或符号性。
还可以将包含一种类型元素的向量转换或强制转换(重新解释)为包含不同类型元素的向量。在 曼德勒X8 函数 _mm256u铸件u si256 函数用于重新解释 __m256型 比较函数的结果作为 __m256i型 用于更新 结果 矢量。
我们用Intel测量了Mandelbrot、矩阵向量乘法和数组平均内核函数的运行时间® AVX/AVX2和Intel® AVX-512内部函数性能比较。源代码是用“/O2”编译的。在我们的测试平台上,Mandelbrot和Intel® AVX-512是1.77倍 1 比英特尔还快® AVX2版本。示例代码可用 在这里 . 数组平均值( 源代码 )是1.91倍 1 更快的矩阵和向量乘法( 源代码 )是1.80倍 1 与AVX2版本相比速度更快。
我们之前说过英特尔可以实现的性能® AVX-512应该是Intel的两倍左右® AVX2型。我们看到我们还没有达到这个数字,有几个原因可以解释为什么加速没有达到预期的值。一种是,较大的向量指令只加快了计算的最内部循环,但总执行时间包括执行外部循环所花费的时间和其他没有加快的开销。另一个潜在的原因是,内存系统的带宽必须在进行计算的所有内核之间共享,而这是一个有限的资源。当大部分带宽被使用时,处理器的计算速度不能超过数据可用的速度。
我们给出了几个如何矢量化数组平均和矩阵向量乘法的例子,以及使用Intel计算Mandelbrot集的代码® AVX2和Intel® AVX-512函数。这段代码是一个更现实的例子,说明如何使用英特尔® AVX-512比我们之前发布的示例代码。从我们测试平台上收集的数据来看® 与Intel相比,AVX-512代码的性能提高了77%到91%® AVX2型。
英特尔® AVX-512充分利用Intel® 与Intel相比,通过将单指令处理的数据翻一番来提高性能的硬件功能® AVX2型。此功能可用于人工智能、深度学习、科学模拟、金融分析、三维建模、图像/音频/视频处理和数据压缩。使用英特尔® AVX-512和解锁您的应用程序的全部潜力。
本文件未授予任何知识产权许可(明示或默示,禁止反悔或其他方式)。
英特尔否认所有明示和暗示的保证,包括但不限于对适销性、特定用途适用性和非侵权性的暗示保证,以及因履行过程、交易过程或贸易惯例而产生的任何保证。
本文件包含有关产品、服务和/或开发过程的信息。 所有资料如有更改,恕不另行通知。请与您的英特尔代表联系,以获取最新的预测、时间表、规格和路线图。
所描述的产品和服务可能包含称为勘误表的缺陷或错误,这些缺陷或错误可能导致与已发布规范的偏差。如有要求,可提供当前的勘误表。
本文件中引用的具有订单号的文件副本可通过以下方式获得: 1-800-548-4725 或者通过拜访 https://www.intel.com/content/www/us/en/design/resource-design-center.html .
Intel、Intel徽标、Core是Intel Corporation在美国和/或其他国家/地区的商标。
*其他名称和品牌可能被称为他人的财产
© 英特尔公司。
§ (1) 如本文件所述§ 性能测试中使用的软件和工作负载可能只针对英特尔微处理器的性能进行了优化。性能测试,如SYSmark和MobileMark,是使用特定的计算机系统、组件、软件、操作和功能来衡量的。这些因素的任何变化都可能导致结果的变化。您应该参考其他信息和性能测试,以帮助您全面评估您计划购买的产品,包括该产品与其他产品结合时的性能。§ 配置:在英特尔® 英特尔台式机® 核心™ i9 7980XE CPU@2.60GHz,64 GB RAM,运行Windows 10企业版10.0.17134.112§ 有关更多信息,请访问 https://www.intel.com/content/www/us/en/benchmarks/benchmark.html