Quantcast
Channel: 英特尔开发人员专区文章
Viewing all 49 articles
Browse latest View live

在不编写 AVX 的情况下使用英特尔® AVX

$
0
0

下载文章

下载在不编写 AVX 的情况下使用英特尔® AVX [PDF 326KB]

简介和工具

英特尔® 高级矢量扩展指令集(AVX)是一套针对英特尔® SIMD 流指令扩展(英特尔® SSE)的 256 位扩展指令集,专为浮点密集型应用而设计。英特尔® SSE 和英特尔® AVX 均为单指令多数据指令集的示例。英特尔® AVX 作为第二代英特尔® 酷睿™ 处理器家族的一部分发布。英特尔® 采用更宽的 256 位矢量 - 一种全新的扩展指令格式(矢量扩展指令集或 VEX)并具备丰富的功能,使系统性能得到显著提升。该指令集架构支持三种操作数,可提升指令编程灵活性,并支持非破坏性的源操作数。传统的 128 位 SIMD 指令也经过了扩展,支持三种操作数和新的指令加密格式 (VEX)。指令加密格式介绍了使用操作码和前缀,以处理器能够理解的格式来表达更高级别的指令的方式。这有助于实现对数据和一般应用的更好的管理,例如图像、音视频处理、科研模拟、金融分析和三维建模与分析。

本文讨论了开发人员可通过哪些方式将英特尔® AVX 集成到应用中,且无需在低级别汇编语言中进行明确地编码。对于 C/C++ 开发人员来说,访问英特尔® AVX 的最直接方式是使用兼容 C 的内部指令。这些内部函数提供了到英特尔® AVX 指令集的访问,以及英特尔® 短矢量数学库 (SVML) 中的更高级别的数学函数。这些函数分别在 immintrin.h 和 ia32intrin.h 头文件中进行声明。应用编程人员还可通过其它方法来使用英特尔® AVX,而且无需将英特尔® AVX 指令添加至其源代码。本文针对这些方法进行了调查(使用英特尔® C++ Composer XE 2011,定位于在 Sandy Bridge 系统上执行)。Linux*、Windows* 和 Mac OS* X 平台均支持英特尔® C++ Composer XE。本文将使用面向 Windows* 平台的命令行开关。本文结尾处的表_ 1 - 英特尔® 编译器命令行开关总结提供了针对每个平台的命令行开关列表。


单指令多数据 (SIMD) 概念回顾

支持矢量或 SIMD 的处理器能够在一次指令中,同时在多个数据操作数上执行一个操作。在一个数字上由另外一个数字执行的操作以生成单个结果的流程被称作标量流程。在 N 个数字上同时执行的操作以生成 N 个结果的流程被称作矢量流程 (N > 1)。英特尔处理器或支持 SIMD 或 AVX 指令的兼容的非英特尔处理器均支持该技术。将算法从标量转化为矢量的流程被称作矢量化。


一般性的 multiplyValues 示例可帮助解释标量和矢量流程(使用英特尔® AVX)之间的区别。

void multiplyValues(float *a, float *b, float * c, int size)
{
	for (i = 0; i < size; i++) {
		a[i] = b[i] * c[i];
	}
}




面向英特尔® AVX 的重新编译

第一个方法是使用 /QaxAVX 编译器开关进行重新编译。无需对源代码进行修改。英特尔® 编译器将生成相应的 128 和 256 位英特尔® AVX VEX 加密指令。当有助于提高性能时,英特尔® 编译器将针对英特尔处理器生成多个特定处理器,且具备自动分布功能的代码路径。最合适的代码将在运行时执行。


编译器自动矢量化

借助合适的架构开关来编译应用,是构建英特尔® AVX 就绪型应用的第一步。借助自动矢量化功能,编译器可代表软件开发人员执行大部分矢量化工作。自动矢量化是满足特定条件时编译器执行的优化。英特尔® C++ 编译器可在生成代码期间自动执行相应的矢量化操作。英特尔® C++ 编译器矢量化指南详细介绍了矢量化。当优化级别为 /O2 或更高时,英特尔编译器将寻找矢量化机遇。

让我们来考虑一个简单的矩阵矢量乘法示例,该示例随英特尔® C++ ComposeXE 提供,详细阐释了矢量化的概念。下列代码片段来自 vec_samples 归档的 Multiply.c 中的 matvec 函数:

void matvec(int size1, int size2, FTYPE a[][size2], FTYPE b[], FTYPE x[])
{
	for (i = 0; i < size1; i++) {
		b[i] = 0;

		for (j = 0;j < size2; j++) {
			b[i] += a[i][j] * x[j];
		}
	}
}

如果没有矢量化,外层循环将执行 size1 时间,内层循环将执行 size1*size2 时间。借助 /QaxAVX 开关实现矢量化以后,内层循环可以展开(unrolled),这是因为可在每次操作的单个指令中执行四次乘法和四次加法。矢量化循环的效率比标量循环高得多。英特尔® AVX 的优势还适用于单精度浮点数字,因为 8 个单精度浮点操作数可以存于 ymm 寄存器中。

循环必须满足特定的标准才能实现矢量化。在运行时进入循环时,必须要知道循环运行次数。运行次数可以是变量,但在执行循环时必须是常量。循环必须具备单进和单出能力,而且退出不能依赖于输入数据。此外还存在一些分支标准,例如不允许开关语句(switch statement)。如果 If 语句可作为隐蔽任务实施,则可允许这种类型的语句。最内层的循环最有可能是矢量化的对象,而且在循环内部使用函数调用可能会影响矢量化。内联函数和固有的 SVML 函数可增加矢量化机遇。

在应用开发的实施和调试阶段,建议对矢量化信息进行检查。英特尔® 编译器提供了矢量化报告,可帮助你了解被矢量化以及未被矢量化的元素。该报告可通过 /Qvec-report=<n> 命令行选项提供,其中 n 指定了报告的详细级别。详细级别随 n 数值的增加而增加。如果 n=3,则可以提供相关性信息、被矢量化的循环和未被矢量化的循环。开发人员可根据报告中的信息来修改实施,循环未被矢量化的原因提供了非常有帮助的信息。

开发人员在其具体应用方面具有深入的专业知识,因此有时可以忽略自动矢量化行为。编译指示提供了额外的信息,以便为自动矢量化流程提供帮助。部分示例包括:一直对循环进行矢量化操作、确定循环内的数据保持一致、忽略潜在的数据相关性等。addFloats 示例对部分重要点进行了说明。你需要检查生成的汇编语言指令,以了解所生成的编译器。当指定 /S 命令行选项时,英特尔编译器将在当前的工作目录中生成汇编文件。

void addFloats(float *a, float *b, float *c, float *d, float *e, int n)	{
	int i;
#pragma simd
#pragma vector aligned
	for(i = 0; i < n; ++i)	{
		a[i] = b[i] + c[i] + d[i] + e[i];
	}
}


请注意 simd 和矢量编译指示的使用。它们在实现所期望的英特尔® AVX 256 位矢量化方面起着重要作用。当使用 /QaxAVX 选项来编译 addFloats 时(没有包含 simd 和矢量编译指示的行),将生成下列代码。

没有 simd 和矢量编译指示

.B46.3::
        vmovss    xmm0, DWORD PTR [rdx+r10*4]
        vaddss    xmm1, xmm0, DWORD PTR [r8+r10*4]  
        vaddss    xmm2, xmm1, DWORD PTR [r9+r10*4]  
        vaddss    xmm3, xmm2, DWORD PTR [rax+r10*4]
        vmovss    DWORD PTR [rcx+r10*4], xmm3
        inc       r10
        cmp       r10, r11
        jl        .B46.3


汇编代码显示的是使用英特尔® 128 位 AVX 指令的标量版本。目标是提供英特尔® 256 位 AVX 指令的打包(“矢量”的另一种说法)版本。Vaddss 中的 ss 表示只将一组单精度浮点操作数添加在一起 - 标量操作。如果使用 vaddps,那么该算法将更加高效;ps 表示在单精度浮点操作数上执行打包操作。

向代码只添加 "#pragma simd"有助于生成英特尔® 128 位 AVX 指令的打包版本。此外,编译器还将展开循环,从而减少与循环测试结束相关的执行指令数量。由于每个指令只运行四个操作数,因此仍有进一步的优化空间。

With #pragma simd

.B46.11::
        vmovups   xmm0, XMMWORD PTR [rdx+r10*4]
        vaddps    xmm1, xmm0, XMMWORD PTR [r8+r10*4]
        vaddps    xmm2, xmm1, XMMWORD PTR [r9+r10*4]
        vaddps    xmm3, xmm2, XMMWORD PTR [rax+r10*4]
        vmovups   XMMWORD PTR [rcx+r10*4], xmm3
        vmovups   xmm4, XMMWORD PTR [16+rdx+r10*4]
        vaddps    xmm5, xmm4, XMMWORD PTR [16+r8+r10*4]
        vaddps    xmm0, xmm5, XMMWORD PTR [16+r9+r10*4]
        vaddps    xmm1, xmm0, XMMWORD PTR [16+rax+r10*4]
        vmovups   XMMWORD PTR [16+rcx+r10*4], xmm1
        add       r10, 8
        cmp       r10, rbp
        jb        .B46.11

指定 "pragma vector aligned"有助于编译器针对所有阵列参考使用一致的数据移动指令。使用 "pragma simd"和 "pragma vector aligned."可生成期望的 256 位英特尔® AVX 指令。英特尔® 编译器选择 vmovups,这是因为当访问第二代英特尔®酷睿TM处理器上的一致内存时,不会出现任何问题。

With #pragma simd and #pragma vector aligned

.B46.4::
        vmovups   ymm0, YMMWORD PTR [rdx+rax*4]
        vaddps    ymm1, ymm0, YMMWORD PTR [r8+rax*4]
        vaddps    ymm2, ymm1, YMMWORD PTR [r9+rax*4]
        vaddps    ymm3, ymm2, YMMWORD PTR [rbx+rax*4]
        vmovups   YMMWORD PTR [rcx+rax*4], ymm3
        vmovups   ymm4, YMMWORD PTR [32+rdx+rax*4]
        vaddps    ymm5, ymm4, YMMWORD PTR [32+r8+rax*4]
        vaddps    ymm0, ymm5, YMMWORD PTR [32+r9+rax*4]
        vaddps    ymm1, ymm0, YMMWORD PTR [32+rbx+rax*4]
        vmovups   YMMWORD PTR [32+rcx+rax*4], ymm1
        add       rax, 16
        cmp       rax, r11
        jb        .B46.4

这展示了英特尔® 编译器的部分自动矢量化能力。矢量化可通过矢量报告确认(simd 声称编译指令),或者通过检查生成的汇编语言指令来确认。如果开发人员对其应用有着深刻的了解,那么编译指令能够为编译器提供进一步的帮助。请参考英特尔® C++ 编译器矢量化指南了解关于英特尔编译器中的矢量化的更多信息。英特尔® C++ 编译器 XE 12.0 用户和参考指南提供了关于使用矢量化、编译指令和编译器开关的额外信息。英特尔编译器可为您完成大部分的矢量化工作,因此您的应用可以随时使用英特尔® AVX。


面向阵列符号(Notation)的英特尔® Cilk™ Plus C/C++ 扩展

面向阵列符号的英特尔® Cilk™ Plus C/C++ 语言扩展是专用于英特尔的语言扩展,适用于算法在阵列上运行的情况,不需要阵列元素之间的特定操作顺序。如果使用阵列符号来表达算法并通过 AVX 开关进行编译,英特尔® 编译器将生成英特尔® AVX 指令。面向阵列符号的 C/C++ 语言扩展旨在帮助用户在其程序中直接表达高级并行矢量阵列操作。这可帮助编译器执行数据相关性分析、矢量化和自动并行化。从开发人员的角度来看,他们将获得更加可预测的矢量化、改进的性能和更高的硬件资源利用率。通过结合使用面向阵列符号的 C/C++ 语言扩展和其它英特尔® CilkTM Plus 语言扩展,有助于简化并行和矢量化应用开发流程。

要实现上述优势,开发人员可以编写标准的 C/C++ 基本函数,以便通过标量句法来表示操作。在不使用面向阵列符号的 C/C++ 语言扩展的情况下调用时,该基本函数可用于在一个元素上进行操作,必须使用"__declspec(vector)"对该基础函数进行声明,以便用户能够通过面向阵列符号的 C/C++ 语言扩展来调用。

multiplyValues 示例作为一个基础函数来展示:

__declspec(vector) float multiplyValues(float b, float c)	
{
	return b*c;
}

该标量调用通过该简单的示例进行说明:

	float a[12], b[12], c[12];
a[j] = multiplyValues(b[j], c[j]);	

此外,借助面向阵列符号的 C/C++ 语言扩展,该函数还可在整个阵列或阵列的一部分上来操作。片段操作符(section operator)可用于要在其上进行操作的阵列部分。句法: [<lower bound> : <length> : <stride>]

下限是源阵列的开始索引、长度是结果阵列的长度,跨度表示的是整个源阵列的跨度。跨度是可选的,默认是一个。

这些阵列部分示例有助于阐释具体的使用方式:

	float a[12];

	a[:] refers to the entire a array
	a[0:2:3] refers to elements 0 and 3 of array a.  
	a[2:2:3] refers to elements 2 and 5 of array a
	a[2:3:3] refers to elements 2, 5, and 8 of array a

此外,符号还支持多维阵列。

	float a2d[12][4];

	a2d[:][:] refers to the entire a2d array
	a[:][0:2:2] refers to elements 0 and 2 of the columns for all rows of a2d.  
	a[2:2:3] refers to elements 2 and 5 of array a
	a[2:3:3] refers to elements 2, 5, and 8 of array a

借助阵列符号,用户可以轻松地调用使用阵列的 multiplyValues。英特尔® 编译器提供了矢量化版本,可以分别执行相应的操作。以下为您列举了部分实例:第一个示例在整个阵列上操作,第二个则在阵列的一个子集或部分上操作。

该示例调用了整个阵列的函数:

a[:] = multiplyValues(b[:], c[:]);	

该示例调用了阵列的一个子集的函数:

a[0:5] = multiplyValues(b[0:5], c[0:5]);	

面向阵列标记的 C/C++ 语言扩展可简化阵列应用的开发。如果采用自动矢量化功能,则需要检查英特尔® 编译器生成的指令,以确认是否正在使用英特尔® AVX 指令。通过 /S 开关进行编译可生成汇编文件。搜索 mutliplyValues 将得到标量和矢量化这两个版本。

标量实施使用 VEX 编码 128 位英特尔® AVX 指令的标量(ss)版本:

        vmulss    xmm0, xmm0, xmm1
        ret  

矢量实施使用 VEX 编码 256 位英特尔® AVX 指令的标量(ps)版本:

	  sub       rsp, 40
        vmulps    ymm0, ymm0, ymm1
        mov       QWORD PTR [32+rsp], r13
        lea       r13, QWORD PTR [63+rsp]
        and       r13, -32
        mov       r13, QWORD PTR [32+rsp]
        add       rsp, 40
        ret

这些简单的示例显示了,面向阵列标记的 C/C++ 语言扩展如何使用英特尔® AVX 的特性,而且不需要开发人员明确地使用任何英特尔® AVX 指令。无论是否使用基础函数,都可以使用面向阵列标记的 C/C++ 语言扩展。该技术使用最新的英特尔® AVX 指令集架构,为开发人员提供了更高的灵活性和更多的选择。请参考英特尔® C++ 编译器 XE 12.0 用户和参考指南,了解面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展的更多信息。

开发人员可使用编译器来生成英特尔® AVX 指令,并对其应用进行自动矢量化处理。此外,他们也可以选择面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展进行开发,以便充分利用英特尔® AVX。开发团队还可以通过另外一种方式在不编写汇编语言的情况下使用英特尔® AVX。英特尔® 集成性能基元库(英特尔® IPP)和英特尔® 数学核心函数库(英特尔® MKL)为开发人员带来了许多优势,包括支持英特尔® AVX 等最新的英特尔技术。


使用英特尔® IPP 和英特尔® MKL 库

借助英特尔® 集成性能基元库和英特尔® 数学核心函数库,英特尔针对多媒体、数据处理、加密和通信应用提供了数千个高度优化的软件函数。这些线程安全库支持多种操作系统,最快的代码将在指定平台上运行。通过这种方式,用户可以轻松地向应用添加多核并行化和矢量化能力,并利用最新的处理器指令来执行代码。英特尔® 集成性能基元库 7.0 包括大约 175 个针对英特尔® AVX 而优化的函数。这些函数可用于执行 FFT、过滤、卷积、重新调整大小等操作。英特尔® 数学核心函数库 10.2 支持面向 BLASS (dgemm)、FFT 和 VML (exp, log, pow) 的英特尔® AVX。实施过程在英特尔® MKL 10.3 中得到了简化,因为开始不再需要调用 mkl_enable_instructions。英特尔® MKL 10.3 可扩展英特尔® AVX,以便支持 DGMM/SGEMM、radix-2 Complex FFT、最真实的 VML 函数以及 VSL 分布生成器。

如果您已经在使用,或者考虑使用这些版本的库,那么您的应用将能够使用英特尔® AVX 指令集。在 Sandy Bridge 平台上运行时,库将执行英特尔® AVX 指令,并且支持 Linux*、Windows* 和 Mac OS* X 平台。

如欲了解关于针对英特尔® AVX 而优化的英特尔® IPP 函数的更多信息,请访问:/zh-cn/articles/intel-ipp-intel-avx

如欲了解关于英特尔® MKL AVX 支持的更多信息,请访问:英特尔® MKL V10.3 中的英特尔® AVX优化


总结

人们对更高计算性能的需求促使英特尔在微架构和指令集领域不断进行创新。应用开发人员希望确保他们的产品能够利用技术上的进步,且无需投入更多的开发资源。本文介绍的方法、工具和库可帮助开发人员从英特尔® 高级矢量扩展指令集的发展上获益,而且无需编写英特尔® AVX 汇编语言。


更多信息和参考资料

英特尔® 高级矢量扩展指令集

英特尔® 编译器

英特尔® C++ Composer XE 2011 文档

如何编译英特尔® AVX

英特尔® C++ 编译器矢量化指南

针对英特尔® 高级矢量扩展指令集而优化的英特尔® 集成性能基元库函数

英特尔® MKL V10.3 中的英特尔® AVX(高级矢量扩展指令集) 优化


表 1 – 英特尔® 编译器命令行开关总结

英特尔® 编译器命令行开关描述Windows*Linux*Mac OS* X
如果存在性能优势,则可以针对英特尔处理器生成多个特定处理器的自动分布代码路径/Qax-ax-ax
生成矢量化报告/Qvec-report<n>-vec-report<n>-vec-report<n>
生成汇编语言文件/S-S-S

在不编写 AVX 代码的情况下使用 AVX

$
0
0

Using AVX Without Writing AVX Code (PDF 260KB)

摘要

英特尔® 高级矢量扩展指令集(AVX)是一套针对英特尔® SIMD 流指令扩展(英特尔® SSE)的 256 位扩展指令集,专为浮点密集型应用而设计。英特尔® SSE 和英特尔® AVX 均为单指令多数据指令集的示例。英特尔® AVX 作为第二代英特尔® 酷睿™ 处理器家族的一部分发布。英特尔® AVX 采用更宽的 256 位矢量 - 一种全新的扩展指令格式(矢量扩展指令集或 VEX)并具备丰富的功能,使系统性能得到显著提升。

该指令集架构支持三种操作数,可提升指令编程灵活性,并支持非破坏性的源操作数。传统的 128 位 SIMD 指令也经过了扩展,支持三种操作数和新的指令加密格式 (VEX)。指令加密格式介绍了使用操作码和前缀,以处理器能够理解的格式来表达更高级别的指令的方式。这有助于实现对数据和一般应用的更好的管理,例如图像、音视频处理、科研模拟、金融分析和三维建模与分析。

本文讨论了开发人员可通过哪些方式将英特尔® AVX 集成到应用中,且无需在低级别汇编语言中进行明确地编码。对于 C/C++ 开发人员来说,访问英特尔® AVX 的最直接方式是使用兼容 C 的内部指令。这些内部函数提供了到英特尔® AVX 指令集的访问,以及英特尔® 短矢量数学库 (SVML) 中的更高级别的数学函数。这些函数分别在 immintrin.h 和 ia32intrin.h 头文件中进行声明。应用编程人员还可通过其它方法来使用英特尔® AVX,而且无需将英特尔® AVX 指令添加至其源代码。本文针对这些方法进行了调查(使用英特尔® C++ Composer XE 2011,定位于在 Sandy Bridge 系统上执行)。Linux*、Windows* 和 Mac OS* X 平台均支持英特尔® C++ Composer XE。本文将使用面向 Windows* 平台的命令行开关。

本文是《英特尔® 多线程应用开发指南》系列文章中的一篇,旨在为开发人员开发适用于英特尔® 平台的高效多线程应用提供指导。

背景

支持矢量或 SIMD 的处理器能够在一次指令中,同时在多个数据操作数上执行一个操作。在一个数字上由另外一个数字执行的操作以生成单个结果的流程被称作标量流程。在 N 个数字上同时执行的操作以生成 N 个结果的流程被称作矢量流程 (N > 1)。英特尔处理器或支持 SIMD 或 AVX 指令的兼容的非英特尔处理器均支持该技术。将算法从标量转化为矢量的流程被称作矢量化。

建议

面向英特尔® AVX 的重新编译

第一个方法是使用 /QaxAVX 编译器开关进行重新编译。无需对源代码进行修改。英特尔® 编译器将生成相应的 128 和 256 位英特尔® AVX VEX 加密指令。当有助于提高性能时,英特尔® 编译器将针对英特尔处理器生成多个特定处理器,且具备自动分布功能的代码路径。最合适的代码将在运行时执行。

编译器自动矢量化

借助合适的架构开关来编译应用,是构建英特尔® AVX 就绪型应用的第一步。借助自动矢量化功能,编译器可代表软件开发人员执行大部分矢量化工作。自动矢量化是满足特定条件时编译器执行的优化。英特尔® C++ 编译器可在生成代码期间自动执行相应的矢量化操作。英特尔® C++ 编译器矢量化指南详细介绍了矢量化。当优化级别为 /O2 或更高时,英特尔编译器将寻找矢量化机遇。

让我们来考虑一个简单的矩阵矢量乘法示例,该示例随英特尔® C++ ComposeXE 提供,详细阐释了矢量化的概念。下列代码片段来自 vec_samples 归档的 Multiply.c 中的 matvec 函数:

如果没有矢量化,外层循环将执行 size1 时间,内层循环将执行 size1*size2 时间。借助 /QaxAVX 开关实现矢量化以后,内层循环可以展开(unrolled),这是因为可在每次操作的单个指令中执行四次乘法和四次加法。矢量化循环的效率比标量循环高得多。英特尔® AVX 的优势还适用于单精度浮点数字,因为 8 个单精度浮点操作数可以存于 ymm 寄存器中。

循环必须满足特定的标准才能实现矢量化。在运行时进入循环时,必须要知道循环运行次数。运行次数可以是变量,但在执行循环时必须是常量。循环必须具备单进和单出能力,而且退出不能依赖于输入数据。此外还存在一些分支标准,例如不允许开关语句(switch statement)。如果 If 语句可作为隐蔽任务实施,则可允许这种类型的语句。最内层的循环最有可能是矢量化的对象,而且在循环内部使用函数调用可能会影响矢量化。内联函数和固有的 SVML 函数可增加矢量化机遇。

在应用开发的实施和调试阶段,建议对矢量化信息进行检查。英特尔® 编译器提供了矢量化报告,可帮助你了解被矢量化以及未被矢量化的元素。该报告可通过 /Qvec-report= 命令行选项提供,其中 n 指定了报告的详细级别。详细级别随 n 数值的增加而增加。如果 n=3,则可以提供相关性信息、被矢量化的循环和未被矢量化的循环。开发人员可根据报告中的信息来修改实施,循环未被矢量化的原因提供了非常有帮助的信息。

开发人员在其具体应用方面具有深入的专业知识,因此有时可以忽略自动矢量化行为。编译指示提供了额外的信息,以便为自动矢量化流程提供帮助。部分示例包括:一直对循环进行矢量化操作、确定循环内的数据保持一致、忽略潜在的数据相关性等。addFloats 示例对部分重要点进行了说明。你需要检查生成的汇编语言指令,以了解所生成的编译器。当指定 /S 命令行选项时,英特尔编译器将在当前的工作目录中生成汇编文件。

请注意 simd 和矢量编译指示的使用。它们在实现所期望的英特尔® AVX 256 位矢量化方面起着重要作用。向代码添加 "#pragma simd"有助于生成英特尔® 128 位 AVX 指令的打包版本。此外,编译器还将展开循环,从而减少与循环测试结束相关的执行指令数量。指定 "pragma vector aligned"有助于编译器针对所有阵列参考使用一致的数据移动指令。使用 "pragma simd"和 "pragma vector aligned."可生成期望的 256 位英特尔® AVX 指令。英特尔® 编译器选择 vmovups,这是因为当访问第二代英特尔®酷睿TM处理器上的一致内存时,使用不一致的转移指令不会出现任何问题。

使 #pragma simd 和 #pragma 矢量保持一致

这展示了英特尔® 编译器的部分自动矢量化能力。矢量化可通过矢量报告确认(simd 声称编译指令),或者通过检查生成的汇编语言指令来确认。如果开发人员对其应用有着深刻的了解,那么编译指令能够为编译器提供进一步的帮助。请参考英特尔® C++ 编译器矢量化指南了解关于英特尔编译器中的矢量化的更多信息。英特尔® C++ 编译器 XE 12.0 用户和参考指南提供了关于使用矢量化、编译指令和编译器开关的额外信息。英特尔编译器可为您完成大部分的矢量化工作,因此您的应用可以随时使用英特尔® AVX。

面向阵列符号(Notation)的英特尔® Cilk™ Plus C/C++ 扩展

面向阵列符号的英特尔® Cilk™ Plus C/C++ 语言扩展是专用于英特尔的语言扩展,适用于算法在阵列上运行的情况,不需要阵列元素之间的特定操作顺序。如果使用阵列符号来表达算法并通过 AVX 开关进行编译,英特尔® 编译器将生成英特尔® AVX 指令。面向阵列符号的 C/C++ 语言扩展旨在帮助用户在其程序中直接表达高级并行矢量阵列操作。这可帮助编译器执行数据相关性分析、矢量化和自动并行化。从开发人员的角度来看,他们将获得更加可预测的矢量化、改进的性能和更高的硬件资源利用率。通过结合使用面向阵列符号的 C/C++ 语言扩展和其它英特尔® CilkTM Plus 语言扩展,有助于简化并行和矢量化应用开发流程。

要实现上述优势,开发人员可以编写标准的 C/C++ 基本函数,以便通过标量句法来表示操作。在不使用面向阵列符号的 C/C++ 语言扩展的情况下调用时,该基本函数可用于在一个元素上进行操作,必须使用“__declspec(vector)”对该基础函数进行声明,以便用户能够通过面向阵列符号的 C/C++ 语言扩展来调用。

multiplyValues 示例作为一个基础函数来展示:

该标量调用通过该简单的示例进行说明:

此外,借助面向阵列符号的 C/C++ 语言扩展,该函数还可在整个阵列或阵列的一部分上来操作。片段操作符(section operator)可用于要在其上进行操作的阵列部分。句法: [ : : ]

下限是源阵列的开始索引、长度是结果阵列的长度,跨度表示的是整个源阵列的跨度。跨度是可选的,默认是一个。

这些阵列部分示例有助于阐释具体的使用方式:

此外,符号还支持多维阵列。

借助阵列符号,用户可以轻松地调用使用阵列的 multiplyValues。英特尔® 编译器提供了矢量化版本,可以分别执行相应的操作。以下为您列举了部分实例:第一个示例在整个阵列上操作,第二个则在阵列的一个子集或部分上操作。

该示例调用了整个阵列的函数:

a[:] = multiplyValues(b[:], c[:]);

该示例调用了阵列的一个子集的函数:

a[0:5] = multiplyValues(b[0:5], c[0:5]);

这些简单的示例显示了,面向阵列标记的 C/C++ 语言扩展如何使用英特尔® AVX 的特性,而且不需要开发人员明确地使用任何英特尔® AVX 指令。无论是否使用基础函数,都可以使用面向阵列标记的 C/C++ 语言扩展。该技术使用最新的英特尔® AVX 指令集架构,为开发人员提供了更高的灵活性和更多的选择。请参考英特尔® C++ 编译器 XE 12.0 用户和参考指南,了解面向阵列标记的英特尔® Cilk™ Plus C/C++ 语言扩展的更多信息。

使用英特尔® IPP 和英特尔® MKL 库

借助英特尔® 集成性能基元库和英特尔® 数学核心函数库,英特尔针对多媒体、数据处理、加密和通信应用提供了数千个高度优化的软件函数。这些线程安全库支持多种操作系统,最快的代码将在指定平台上运行。通过这种方式,用户可以轻松地向应用添加多核并行化和矢量化能力,并利用最新的处理器指令来执行代码。英特尔® 集成性能基元库 7.0 包括大约 175 个针对英特尔® AVX 而优化的函数。这些函数可用于执行 FFT、过滤、卷积、重新调整大小等操作。英特尔® 数学核心函数库 10.2 支持面向 BLASS (dgemm)、FFT 和 VML (exp, log, pow) 的英特尔® AVX。实施过程在英特尔® MKL 10.3 中得到了简化,因为开始不再需要调用 mkl_enable_instructions。英特尔® MKL 10.3 可扩展英特尔® AVX,以便支持 DGMM/SGEMM、radix-2 Complex FFT、最真实的 VML 函数以及 VSL 分布生成器。

如果您已经在使用,或者考虑使用这些版本的库,那么您的应用将能够使用英特尔® AVX 指令集。在 Sandy Bridge 平台上运行时,库将执行英特尔® AVX 指令,并且支持 Linux*、Windows* 和 Mac OS* X 平台。

如欲了解关于针对英特尔® AVX 而优化的英特尔® IPP 函数的更多信息,请访问:/en-us/articles/intel-ipp-functions-optimized-for-intel-avx-intel-advanced-vector-extensions。如欲了解关于英特尔® MKL AVX 支持的更多信息,请访问:Intel® AVX Optimization in Intel® MKL V10.3

使用准则

人们对更高计算性能的需求促使英特尔在微架构和指令集领域不断进行创新。应用开发人员希望确保他们的产品能够利用技术上的进步,且无需投入更多的开发资源。本文介绍的方法、工具和库可帮助开发人员从英特尔® 高级矢量扩展指令集的发展上获益,而且无需编写英特尔® AVX 汇编语言。

其它资源

利用英特尔高级矢量扩展指令集进行 Wiener 过滤

$
0
0

1 内容简介
英特尔® 高级矢量扩展指令集(英特尔® AVX)是一套针对英特尔® SIMD 流指令扩展(英特尔® SSE)的 256 位扩展指令集,专为浮点密集型应用而设计。对于过分依赖浮点运算的应用,如 3D 几何、视频处理、图像处理和空间 (3D) 音频,这些指令为加快其运行速度提供了一种方法。本应用注释介绍了 Wiener 过滤,并包含一个已经使用英特尔® AVX 进行优化的代码示例。本文中代码和应用注释的源代码参考为利用 SIMD 流指令扩展 [4] 的 AP-807 Wiener 过滤。原始文章包含利用 SIMD 流指令扩展的优化。此外,本文还介绍了如何将代码迁移到 256 位扩展指令集(即英特尔® AVX)。


2 Wiener 过滤器算法
Wiener 过滤也称为最小均方过滤,是一种用于消除图像中不必要的噪声的技术。对该算法的描述摘自 Harley R Myler 和 Aruthur R. Weeks [1] 编著的《C 语言中图像处理算法手册》(Pocket Handbook of ImageProcessing Algorithms in C)。该算法包含四个(经傅里叶变换)矢量输入值,分别代表(一部分)源图像(图像)、降质图像 (Guv)、噪声图像光谱(噪声)和退化函数 (Huv)。每个输入值均是一个 row*col 复数矢量。该复数表示为两个连续的浮点数,代表数字的真实部分和想象部分。计算结果中还包括另一个参数 gamma。当 gamma 为 1.0 时,过滤器被认为是非参数型。直到经过过滤的图像满足要求,才能对过滤器的参数进行调整。


2.1 支持 Wiener 过滤器的应用
Wiener 过滤器通常用在图像处理应用中,用于消除重建图像中的噪声。Wiener 过滤通常用于还原模糊图像。然而,经证实 Wiener 过滤器在自适应滤波中非常重要,曾用于微波变换,并且在通信和其它 DSP 相关领域中得到应用。读者还应意识到傅里叶变换是所有信号处理领域中的一个关键要素。如欲了解有关如何实施傅里叶变换的更多信息,请参考英特尔应用注释 Split-Radix FFT (AP-808)。


2.2 实施 Wiener 过滤
如 2.1 节中所述,函数的输入值为四组复数。对于图像的每一部分而言,以下运算均采用复数算法进行。复杂变量 D 和 Hs 为运算过程中使用的中间变量。函数 Complex_conj 用于提取复数的复共轭。进行除法运算时,必须利用 if 语句进行检查,确保分母不为零。当分母为零时,应将结果设置为零。
1. 复杂噪声 = gamma *(噪声* Complex_conj(噪声))
2. 复杂变量 D = 图像 * Complex_conj(图像)
3. 复杂变量 D = 噪声/D
4. 复杂变量 Hs = Huv * Complex_conj ( Huv )
5. 复数 = Complex_conj ( Huv ) * Guv
6. 复杂图像 = 数字/(Hs + D)


3 利用 128 位 SIMD 对代码进行矢量化
本文的参考源代码(C 版本和 128 位 SIMD 版本)来自英特尔应用注释 AP-807。以下内容介绍了标量代码到 128 位 SIMD 的原始端口。首先,该代码优化简单,只需观察到运算中的许多步骤均涉及到用一个数字乘以其复共轭即可。由于运算结果不包含想象部分,因此 2.2 节中指出的许多运算步骤可以简化。由此产生的 C 代码将在第 5 节中提供。在针对英特尔® SSE 优化代码前,必须将代码转换为适合于 SIMD 执行的形式。将源 C 代码的四次迭代集中在一起,并在新循环的单个迭代中处理。每完成一次新循环相当于完成四次原始迭代。根据 2.2 节中列出的运算,每次迭代过程必须进行三次除法运算。其中一次除法运算发生在步骤 3(由于想象部分为零,因此想象部分无需进行除法运算),另外两次除法运算发生在步骤 6。通过注释后两次除法运算包含相同的分母,可省略其中的一次除法运算。通过使用掩蔽技术,可删除需要检查是否存在为零分母的 if 语句。通过将所有除法运算替换为倒数近似,可实现进一步改进。下面将介绍这些技术。将代码转换为 SIMD 格式后,可通过以下方法省略对分母为零(if 语句)的检查:为非零分母元素创建掩码,将该掩码添加到除法运算的结果中,以清除被零相除的元素。通过掩蔽 MXCSR 寄存器中的 SIMD 浮点异常,避免发生浮点被零除的异常,此技术假设产生的是 QNAN 而非 SNAN。例如,假设您希望计算数量( N / D ),其中 N 和 D 为浮点。典型代码顺序如下所示:

If ( D != 0 )
Result = N / D;
Else
Result = 0.0;

结果可以使用 (and ( div ( N, D ), cmp_neq ( D, 0 ) ) ) 计算得出,而无需使用 if 语句。使用内在函数 (intrinsic) 时,表达如下:

mm_and_ps ( mm_div_ps ( N, D ), mm_cmpneq_ps ( D, zero ) ).

该技术专用于矢量代码,在代码的附加内在函数和汇编语言两个版本中实施。

Newton-Raphson 方法是近似函数的一种经典技术。首先,利用 rcpps 指令计算出倒数的初始“猜测值”。接着,利用 Newton-Raphson 方法对“猜测值”进行改进。由此得出的结果虽不及除法指令提供的准确,但是获得结果的速度要快得多。(程序员必须确定其应用是否允许精确度较低的答案。)有关此技术的详细信息和介绍可在 Newton-Raphson 应用注释 [2] 中找到。Wiener 过滤器采用的特定代码顺序如下所示:(必须对分母进行检查,确保不会出现被零除的现象。)

RC = _mm_rcpps( D );
RECIP = _mm_sub( _mm_add( RC, RC ), _mm_mul( RC, _mm_mul( RC, D ) ) );


4 利用 256 位 SIMD 对代码进行矢量化
通过迁移更改,即可将 128 位矢量代码轻松移植到 256 位 SIMD。256 位代码将在一次循环中进行 8 次迭代,而 128 位英特尔® SSE 在一次循环中只进行 4 次迭代。现在,负载/存储指令和 rcp/mul/add/sub 指令将对 8 个数据点进行运算。上述无分支技术在英特尔® AVX 代码中仍然保留。从代码中可以看出,移植采用相应的英特尔® AVX 内在函数(例如针对 256 位的 _mm256_mul_ps 和针对 128 位的 _mm_mul_ps)完成。6.3 节中提供了完整的 256 位英特尔® AVX 代码。


5 对输入/输出数组进行分组
此外,按照顺序次序对输入和输出数组进行分组可提升 256 位 SIMD 代码的性能。通过高速缓存/内存连续访问内存可减少潜在的高速缓存路径冲突 (cache way conflict)。这为 CPU 硬件预取器提供了一种更为简单的访问模式,从而提高了数据的精确度。


6 结论

对于大量迭代处理,128 位 SIMD 代码与 256 位 SIMD 代码的性能结果(在 CPU 时钟周期方面)比较如下:

 

英特尔® AVX(256 位)

英特尔® SSE (128-bit)

英特尔® AVX 对比 英特尔® SSE

Wiener过滤器

45871

66933

1.46倍

包含分组数组的 Wiener 过滤器

42464

64473

1.51倍


相对于英特尔® SSE,英特尔® AVX 的 Wiener 过滤性能整体上提升了 1.46 倍。与采用 128 位 SIMD 编码相比,英特尔® AVX 大幅提升了 Wiener 过滤算法的性能。通过对输入/输出数组进行分组,英特尔® AVX 的 Wiener 过滤性能在英特尔® SSE 的基础上整体提升了 1.51 倍。本文中提到的提升是采用几种技术的结果。其中包括采用英特尔® AVX(矢量化代码)和通过利用英特尔® SSE 和英特尔® AVX 提供的遮罩运算消除条件分支指令(if 语句)。如果可以接受数值精度较低的结果,则可通过将除法运算替换为倒数近似(采用 Newton-Raphson 技术)实现进一步的性能提升。此外,本文还重点介绍了如何将现有浮点代码轻松移植到英特尔® AVX。


7 代码示例

/*
* Wiener Filter (also known as the Least Mean Square filter)
*
* Reference: The Pocket Handbook of Image Processing Algorithms in C
* by Harley R Myler & Arthur R. Weeks
* 1993 Prentice-Hall, ISBN 0-13-642240-3 p260-3.
*
* The data is several arrays of complex floats in row major order.
* The description for the algorithm from p260 states:
*
* The algorithm computes a parametric Wiener filter on the
* Fourier transform of a degraded image, Guv, with noise
* spectra N, degradation function Huv, and original image Img.
* The computation is in place, so that the filtered version of
* the input is returned in the original image variable. The
* original and noise images are either estimations form some
* predictive function or ad hoc approximations. If the noise
* image is zero, the process reduces to the inverse filter.
*
* The Weiner parameter gamma is passed to the algorithm.
* If this parameter is 1.0, the filter is non-parametric.
* Methods exist in the literature to derive the parameter value;
* however, it is sometimes determined from trial and error.
*
*NOTE!!!! The code on page 263 has an error. In cxml, the complex
* multiply routine, the imaginary part of the computation should be
* a*d + b*c, not a*d - b*c.
*
*NOTE! (another error) The *complex* array length is rows*cols, so the
* *float* array length should be 2*rows*cols. Also, note that the
* algorithm operates on one component of the pixel.
*/
void wiener_filter ( float *Img,
float *Huv,
float *No,
float *Guv,
float gamma,
int rows,
int cols)
{
int i, sz;
float numr, numi, dr, hsr;
sz = 2 * rows * cols;
for (i = 0; i < sz; i += 2)
{
/* Compute (in place) the noise spectral density with Wiener gamma*/
No[i] = (float) ( gamma * ( No[i]*No[i] + No[i+1]*No[i+1] ) );
No[i+1] = (float) 0.0;
/* Compute image spectral density */
dr = (float) ( Img[i]*Img[i] + Img[i+1]*Img[i+1] );
/* Compute denominator spectral density term */
if (dr != 0.0)
dr = (float) (No[i] / dr) ;
/* Compute degradation power spectrum */
hsr = (float) ( Huv[i]*Huv[i] + Huv[i+1]*Huv[i+1] );
/* Compute numerator term */
numr = (float) ( Huv[i]*Guv[i] + Huv[i+1]*Guv[i+1] );
numi = (float) ( Huv[i]*Guv[i+1] - Huv[i+1]*Guv[i ] );
/* Final computation */
if ( (hsr + dr) != 0.0 )
{
Img[i] = (float) (numr / (hsr + dr));
Img[i+1] = (float) (numi / (hsr + dr));
}
else
{
Img[i] = (float) 0.0;
Img[i+1] = (float) 0.0;
}
}
} /* wiener_filter */



7.2 128 位内在代码 (intrinsics code)

/*
#include 
//#define MM_FUNCTIONALITY
#include 
#include 
void intrin_wiener_rcp_sse( float *Img,
float *Huv,
float *No,
float *Guv,
float gamma,
int rows,
int cols )
{
int i, sz;
__m128 first2, next2, nor4, noi4, nr4, inr4, ini4, dr4;
__m128 hr4, hi4, hsr4, gr4, gi4, numr4, numi4;
__m128 rc, denom;
__m128 zero = _mm_set_ps1 (0.0);
sz = 2 * rows * cols;
assert( (sz > 3) & !(sz & 3) );
assert( !( ((int)Img) & 15 ) ); /* Assume alignment */
assert( !( ((int)Huv) & 15 ) );
assert( !( ((int)No) & 15 ) );
assert( !( ((int)Guv) & 15 ) );
for (i = 0; i < sz; i += 8)
{
* Compute (in place) the noise spectral density with Wiener gamma
*
* complex Noise = gamma * (Noise * complex conj Noise)
*
* No[i] = (float) ( gamma * ( No[i]*No[i] + No[i+1]*No[i+1] ) );
* No[i+1] = (float) 0.0;
*/
first2 = _mm_load_ps ( &No[i] );
next2 = _mm_load_ps ( &No[i+4] );
nor4 = _mm_shuffle_ps( first2, next2, 0x88 );
noi4 = _mm_shuffle_ps( first2, next2, 0xdd );
nr4 = _mm_mul_ps ( _mm_set_ps1( gamma ) ,
_mm_add_ps ( _mm_mul_ps( nor4 , nor4 ),
_mm_mul_ps( noi4 , noi4 ) ) );
_mm_store_ps( &No[i ], _mm_unpacklo_ps ( nr4, zero ) );
_mm_store_ps( &No[i+4], _mm_unpackhi_ps ( nr4, zero ) );
/*
* Compute image spectral density
*
* Complex D = Image * complex conj Image
*
* dr = (float) ( Img[i]*Img[i] + Img[i+1]*Img[i+1] );
*/
first2 = _mm_load_ps ( &Img[i] );
next2 = _mm_load_ps ( &Img[i+4] );
inr4 = _mm_shuffle_ps( first2, next2, 0x88 );
ini4 = _mm_shuffle_ps( first2, next2, 0xdd );
dr4 = _mm_add_ps ( _mm_mul_ps( inr4 , inr4),
_mm_mul_ps( ini4 , ini4) );
/*
* Compute denominator spectral density term
*
* Complex D = noise / D
*
* if (dr != 0.0)
* dr = (float) (No[i] / dr) ;
*
* Do that reciprical division thing!
*/
rc = _mm_rcp_ps(dr4);
rc = _mm_sub_ps( _mm_add_ps( rc, rc),
_mm_mul_ps( rc, _mm_mul_ps( rc, dr4) ) );
dr4 = _mm_and_ps ( _mm_mul_ps ( nr4 , rc ),
_mm_cmpneq_ps( dr4, zero ) );
/*
* Compute degradation power spectrum
*
* Complex Hs = Huv * complex conj Huv
*
* hsr = (float) ( Huv[i]*Huv[i] + Huv[i+1]*Huv[i+1] );
*/
first2 = _mm_load_ps ( &Huv[i] );
next2 = _mm_load_ps ( &Huv[i+4] );
hr4 = _mm_shuffle_ps( first2, next2, 0x88 );
hi4 = _mm_shuffle_ps( first2, next2, 0xdd );
hsr4 = _mm_add_ps ( _mm_mul_ps (hr4 , hr4 ),
_mm_mul_ps (hi4 , hi4 ) );
/*
* Compute numerator term
*
* Complex Num = complex conj Huv * Guv
*
* numr = (float) ( Huv[i]*Guv[i] + Huv[i+1]*Guv[i+1] );
* numi = (float) ( Huv[i]*Guv[i+1] - Huv[i+1]*Guv[i ] );
*/
first2 = _mm_load_ps ( &Guv[i] );
next2 = _mm_load_ps ( &Guv[i+4] );
gr4 = _mm_shuffle_ps( first2, next2, 0x88 );
gi4 = _mm_shuffle_ps( first2, next2, 0xdd );
numr4 = _mm_add_ps ( _mm_mul_ps (hr4 , gr4),
_mm_mul_ps (hi4 , gi4) );
numi4 = _mm_sub_ps ( _mm_mul_ps (hr4 , gi4),
_mm_mul_ps (hi4 , gr4) );
/*
* Final computation
*
* Complex Image = Num / (Hs + D)
*
* if ( (hsr + dr) != 0.0 )
* {
* Img[i] = (float) (numr / (hsr + dr));
* Img[i+1] = (float) (numi / (hsr + dr));
* }
* else
* {
* Img[i] = (float) 0.0;
* Img[i+1] = (float) 0.0;
* }
*
* Do the reciprical division thing
*/
denom = _mm_add_ps( hsr4, dr4 );
rc = _mm_rcp_ps(denom);
rc = _mm_sub_ps( _mm_add_ps( rc, rc),
_mm_mul_ps( rc, _mm_mul_ps( rc, denom) ) );
inr4 = _mm_and_ps( _mm_mul_ps ( numr4 , rc ) ,
_mm_cmpneq_ps( denom, zero ) );
ini4 = _mm_and_ps( _mm_mul_ps ( numi4 , rc ) ,
_mm_cmpneq_ps( denom, zero ) );
_mm_store_ps( &Img[i ], _mm_unpacklo_ps ( inr4, ini4 ) );
_mm_store_ps( &Img[i+4], _mm_unpackhi_ps ( inr4, ini4 ) );
}
} /* intrin_wiener_rcp */


    


7.3 256 位内在代码 (intrinsics code)

void intrin_wiener_rcp_avx( float *Img,
					   float *Huv,
					   float *No,
					   float *Guv,
					   float gamma,
					   int rows,
					   int cols )
{
	int i, sz;
	__m256 first2, next2, nor4, noi4, nr4, inr4, ini4, dr4;
	__m256 hr4, hi4, hsr4, gr4, gi4, numr4, numi4;
	__m256 rc, denom;
	__m256 zero = _mm256_setzero_ps();
	sz = 2 * rows * cols;
	assert( (sz > 3) & !(sz & 3) );
	assert( !( ((int)Img) & 15 ) ); /* Assume alignment */
	assert( !( ((int)Huv) & 15 ) );
	assert( !( ((int)No) & 15 ) );
	assert( !( ((int)Guv) & 15 ) );
	for (i = 0; i < sz; i += 16)
	{
		* Compute (in place) the noise spectral density with Wiener gamma
		*
		* complex Noise = gamma * (Noise * complex conj Noise)
		*
		* No[i] = (float) ( gamma * ( No[i]*No[i] + No[i+1]*No[i+1] ) );
		* No[i+1] = (float) 0.0;
		*/
		first2 = _mm256_load_ps ( &No[i] );
		next2 = _mm256_load_ps ( &No[i+4*2] );
		nor4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		noi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		nr4 = _mm256_mul_ps ( _mm256_set1_ps( gamma ) ,
		_mm256_add_ps ( _mm256_mul_ps( nor4 , nor4 ),
		_mm256_mul_ps( noi4 , noi4 ) ) );
		_mm256_store_ps( &No[i ], _mm256_unpacklo_ps ( nr4, zero ) );
		_mm256_store_ps( &No[i+4*2], _mm256_unpackhi_ps ( nr4, zero ) );
		
		/*
		* Compute image spectral density
		*
		* Complex D = Image * complex conj Image
		*
		* dr = (float) ( Img[i]*Img[i] + Img[i+1]*Img[i+1] );
		*/
		first2 = _mm256_load_ps ( &Img[i] );
		next2 = _mm256_load_ps ( &Img[i+4*2] );
		inr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		ini4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		dr4 = _mm256_add_ps ( _mm256_mul_ps( inr4 , inr4),
		_mm256_mul_ps( ini4 , ini4) );
		/*
		* Compute denominator spectral density term
		*
		* Complex D = noise / D
		*
		* if (dr != 0.0)
		* dr = (float) (No[i] / dr) ;
		*
		* Do that reciprical division thing!
		*/
		rc = _mm256_rcp_ps(dr4);
		rc = _mm256_sub_ps( _mm256_add_ps( rc, rc),
		_mm256_mul_ps( rc, _mm256_mul_ps( rc, dr4) ) );
		dr4 = _mm256_and_ps ( _mm256_mul_ps ( nr4 , rc ),
		_mm256_cmpneq_ps( dr4, zero ) );
		/*
		* Compute degradation power spectrum
		*
		* Complex Hs = Huv * complex conj Huv
		*
		* hsr = (float) ( Huv[i]*Huv[i] + Huv[i+1]*Huv[i+1] );
		*/
		first2 = _mm256_load_ps ( &Huv[i] );
		next2 = _mm256_load_ps ( &Huv[i+4*2] );
		hr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		hi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		hsr4 = _mm256_add_ps ( _mm256_mul_ps (hr4 , hr4 ),
		_mm256_mul_ps (hi4 , hi4 ) );
		/*
		* Compute numerator term
		*
		* Complex Num = complex conj Huv * Guv
		*
		* numr = (float) ( Huv[i]*Guv[i] + Huv[i+1]*Guv[i+1] );
		* numi = (float) ( Huv[i]*Guv[i+1] - Huv[i+1]*Guv[i ] );
		*/
		first2 = _mm256_load_ps ( &Guv[i] );
		next2 = _mm256_load_ps ( &Guv[i+4*2] );
		gr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		gi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		numr4 = _mm256_add_ps ( _mm256_mul_ps (hr4 , gr4),
		_mm256_mul_ps (hi4 , gi4) );
		numi4 = _mm256_sub_ps ( _mm256_mul_ps (hr4 , gi4),
		_mm256_mul_ps (hi4 , gr4) );
		/*
		* Final computation
		*
		* Complex Image = Num / (Hs + D)
		*
		* if ( (hsr + dr) != 0.0 )
		AP-807 Wiener Filtering Using Streaming SIMD Extensions
		01/28/99 15
		* {
		* Img[i] = (float) (numr / (hsr + dr));
		* Img[i+1] = (float) (numi / (hsr + dr));
		* }
		* else
		* {
		* Img[i] = (float) 0.0;
		* Img[i+1] = (float) 0.0;
		* }
		*
		* Do the reciprical division thing
		*/
		denom = _mm256_add_ps( hsr4, dr4 );
		rc = _mm256_rcp_ps(denom);
		rc = _mm256_sub_ps( _mm256_add_ps( rc, rc),
		_mm256_mul_ps( rc, _mm256_mul_ps( rc, denom) ) );
		inr4 = _mm256_and_ps( _mm256_mul_ps ( numr4 , rc ) ,
		_mm256_cmpneq_ps( denom, zero ) );
		ini4 = _mm256_and_ps( _mm256_mul_ps ( numi4 , rc ) ,
		_mm256_cmpneq_ps( denom, zero ) );
		_mm256_store_ps( &Img[i ], _mm256_unpacklo_ps ( inr4, ini4 ) );
		_mm256_store_ps( &Img[i+4*2], _mm256_unpackhi_ps ( inr4, ini4 ) );

	}
} /* intrin_wiener_rcp */


    


7.4 包含分组数组的 256 位内在代码 (intrinsics code)

blockHNG Structure

Huv[0]Huv[15]No[0]No[15]Guv[0]Guv[15]Huv[16]Huv[31]No[16]
void intrin_wiener_rcp_avx ( float *Img,
					float *_blockHNG,
					float gamma,
					int rows,
					int cols)
{
	int sz;
	__m256 first2, next2, nor4, noi4, nr4, inr4, ini4, dr4;
	__m256 hr4, hi4, hsr4, gr4, gi4, numr4, numi4;
	__m256 rc, denom;
	__m256 zero = _mm256_setzero_ps();
	sz = 2 * rows * cols;

	assert( (sz > 3) & !(sz & 3) );
	assert( !( ((int)Img) & 15 ) ); // Assume alignment 
	assert( !( ((int)_blockHNG) & 15 ) ); // Assume alignment 

	float *Huv;
	float *No;
	float *Guv;
	
	int j = 0;	// img index
	for (int _blockHNG_tracker = 0; _blockHNG_tracker < 2 * rows * cols * 3; _blockHNG_tracker += 48)
	{
		Huv = &(_blockHNG[_blockHNG_tracker]);
		No = &(_blockHNG[_blockHNG_tracker + 16]);
		Guv = &(_blockHNG[_blockHNG_tracker + 32]);

		/*
		* Compute (in place) the noise spectral density with Wiener gamma
		*
		* complex Noise = gamma * (Noise * complex conj Noise)
		*
		AP-807 Wiener Filtering Using Streaming SIMD Extensions
		01/28/99 13
		* No[i] = (float) ( gamma * ( No[i]*No[i] + No[i+1]*No[i+1] ) );
		* No[i+1] = (float) 0.0;
		*/
		first2 = _mm256_load_ps ( &No[0] );
		next2 = _mm256_load_ps ( &No[8] );
		nor4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		noi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		nr4 = _mm256_mul_ps ( _mm256_set1_ps( gamma ) ,
		_mm256_add_ps ( _mm256_mul_ps( nor4 , nor4 ),
		_mm256_mul_ps( noi4 , noi4 ) ) );

		_mm256_store_ps( &No[0], _mm256_unpacklo_ps ( nr4, zero ) );
		_mm256_store_ps( &No[8], _mm256_unpackhi_ps ( nr4, zero ) );

		/*
		* Compute image spectral density
		*
		* Complex D = Image * complex conj Image
		*
		* dr = (float) ( Img[i]*Img[i] + Img[i+1]*Img[i+1] );
		*/
		first2 = _mm256_load_ps ( &Img[j] );
		next2 = _mm256_load_ps ( &Img[j+8] );
		inr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		ini4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		dr4 = _mm256_add_ps ( _mm256_mul_ps( inr4 , inr4),
		_mm256_mul_ps( ini4 , ini4) );
		/*
		* Compute denominator spectral density term
		*
		* Complex D = noise / D
		*
		* if (dr != 0.0)
		* dr = (float) (No[i] / dr) ;
		*
		* Do that reciprical division thing!
		*/
		rc = _mm256_rcp_ps(dr4);
		rc = _mm256_sub_ps( _mm256_add_ps( rc, rc),
		_mm256_mul_ps( rc, _mm256_mul_ps( rc, dr4) ) );
		dr4 = _mm256_and_ps ( _mm256_mul_ps ( nr4 , rc ),
		_mm256_cmpneq_ps( dr4, zero ) );
		/*
		* Compute degradation power spectrum
		*
		* Complex Hs = Huv * complex conj Huv
		*
		* hsr = (float) ( Huv[i]*Huv[i] + Huv[i+1]*Huv[i+1] );
		*/
		first2 = _mm256_load_ps ( &Huv[0] );
		next2 = _mm256_load_ps ( &Huv[8] );
		hr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		hi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		hsr4 = _mm256_add_ps ( _mm256_mul_ps (hr4 , hr4 ),
		_mm256_mul_ps (hi4 , hi4 ) );
		/*
		* Compute numerator term
		*
		* Complex Num = complex conj Huv * Guv
		*
		* numr = (float) ( Huv[i]*Guv[i] + Huv[i+1]*Guv[i+1] );
		* numi = (float) ( Huv[i]*Guv[i+1] - Huv[i+1]*Guv[i ] );
		*/
		first2 = _mm256_load_ps ( &Guv[0] );
		next2 = _mm256_load_ps ( &Guv[8] );
		gr4 = _mm256_shuffle_ps( first2, next2, 0x88 );
		gi4 = _mm256_shuffle_ps( first2, next2, 0xdd );
		numr4 = _mm256_add_ps ( _mm256_mul_ps (hr4 , gr4),
		_mm256_mul_ps (hi4 , gi4) );
		numi4 = _mm256_sub_ps ( _mm256_mul_ps (hr4 , gi4),
		_mm256_mul_ps (hi4 , gr4) );
		/*
		* Final computation
		*
		* Complex Image = Num / (Hs + D)
		*
		* if ( (hsr + dr) != 0.0 )
		AP-807 Wiener Filtering Using Streaming SIMD Extensions
		01/28/99 15
		* {
		* Img[i] = (float) (numr / (hsr + dr));
		* Img[i+1] = (float) (numi / (hsr + dr));
		* }
		* else
		* {
		* Img[i] = (float) 0.0;
		* Img[i+1] = (float) 0.0;
		* }
		*
		* Do the reciprical division thing
		*/
		denom = _mm256_add_ps( hsr4, dr4 );
		rc = _mm256_rcp_ps(denom);
		rc = _mm256_sub_ps( _mm256_add_ps( rc, rc),
		_mm256_mul_ps( rc, _mm256_mul_ps( rc, denom) ) );
		inr4 = _mm256_and_ps( _mm256_mul_ps ( numr4 , rc ) ,
		_mm256_cmpneq_ps( denom, zero ) );
		ini4 = _mm256_and_ps( _mm256_mul_ps ( numi4 , rc ) ,
		_mm256_cmpneq_ps( denom, zero ) );

		_mm256_store_ps( &Img[j ], _mm256_unpacklo_ps ( inr4, ini4 ) );
		_mm256_store_ps( &Img[j+8], _mm256_unpackhi_ps ( inr4, ini4 ) );
		j+=16;
	}
} /* Intrin_wiener_rcp_avx */



致谢
作者在编著本白皮书时,曾受到 Phil Kerly、Raghu Muthyalampalli 和 Justin Landon 的大力支持。他们帮助作者评估代码性能、提供性能建议以及审阅此白皮书。作者在此表示衷心感谢。

参考资料
本应用注释参考了以下文档,旨在为理解本文提到的主题提供背景或支持信息。

1. 《C 语言中图像处理算法手册》(The Pocket Handbook of Image Processing Algorithms in C), 作者:Harley R Myler 和 Aruthur R.
Weeks. ISBN 0-13-642240-3.
2. 《利用 Newton-Raphson 方法提高倒数和倒数方根指令结果的精确度》(Increasing the Accuracy of the Results from the Reciprocal and Reciprocal Square RootInstructions using the Newton-Raphson Method), 英特尔应用注释(AP-803,编号: 243637-001)。
3. 《Split-Radix FFT》, 英特尔应用注释(AP-808,编号: 243642-001)。
4. 《利用 SIMD 流指令扩展进行 Wiener 过滤》(Wiener Filtering Using Streaming SIMD Extensions), 英特尔应用注释(AP-807)。

利用英特尔® SIMD 流指令扩展和英特尔® 高级矢量扩展指令集的图像处理加速技术

$
0
0

内容简介


现代英特尔处理器通过使用 SIMD(单指令多数据)指令集来实现加速。SIMD 指令集包括广泛的可用英特尔® SIMD 流指令扩展(英特尔® SSE)指令集和全新的英特尔® 高级矢量扩展(英特尔® AVX)指令集。图像处理数据结果和算法通常是利用这些指令集进行优化的理想之选。与英特尔® C++ 编译器自动矢量化循环的功能结合使用时,它为提高图像处理应用的性能提供了一种高效的方法。
本文中,我们将详细介绍一些知名的转换技术,并会提供一些用于说明如何充分利用英特尔® SSE 和英特尔® AVX 转换图像数据的代码示例,同时还将介绍图像处理算法的编译器自动矢量化信息。本文详细介绍了如何优化实施(利用各种数据类型和大小)数据转换和算法以及如何分析比较性能,并为测量英特尔® SSE 优化代码和估算英特尔® AVX 优化代码提供了更快的方法。

英特尔® AVX 是一套针对英特尔® SSE 的 256 位扩展指令集,专为浮点密集型应用而设计。英特尔® AVX 将所有 16 XMM 寄存器扩展到 256 位 YMM 寄存器,使寄存器的带宽加倍,从而能够比 128 位 SIMD 指令提供更高的性能和能效。利用英特尔® AVX 还能够减少寄存器拷贝数,更加高效地使用寄存器和减少代码量。

如以下的性能加速总结所示,利用建议的技术我们能够实现更好的性能加速。


过滤器

英特尔® SSE 加速

英特尔® AVX 加速

Sepia (int base)

2.6倍

3.1倍

Sepia (float base)

1.9倍

2.2倍

Crossfade (int base)

2.7倍

3.6倍

Crossfade (float base)

1.9倍

2.4倍

以上结果系测量建议数据块大小约为 50000 像素的英特尔® 酷睿TM i7 处理器得出。注意英特尔® AVX 性能系采用模拟器估算得出,未考虑未来的架构改进。


概述


本文中提供的代码示例假设使用的是英特尔® C++ 编译器,并且需要对 SIMD、英特尔® SSE 指令内部函数 (intrinsics) 以及如何执行自动矢量化有基本的了解。编译器特性、选项和程序适合于使用英特尔® C++ 编译器 11.1.35 或支持诸如英特尔® AVX 等全新指令集的更高版本。
代码示例出自 C++ 编译器,基于 Microsoft Windows*(Vista 和 XP)进行构建和分析。

范围和假设:

  1. 图像用未压缩的 RGBA 像素值表示,其中每个色彩通道用一个整数(8 位)或一个浮点数(32 位)来表示
  2. 为了简化转换,用存储在 8 位整数或 32 位浮点数中的 0 至255 之间的数字来表示色彩值。
  3. 除非数据处理需要使用英特尔® AVX(使用 32 字节对齐),否则将数据分配给 16 个字节。

有关性能/加速的说明:

  • 由于采用英特尔® AVX 的英特尔处理器尚未上市,因此本文中利用英特尔® AVX 的函数的性能为估计值。架构模拟器(英特尔® 软件开发模拟器)和模拟器(英特尔® 架构代码分析器)用于验证行为和估算英特尔® AVX 性能
  • 实际性能取决于处理器架构、高速缓存配置和大小以及频率等。
  • 本文仅提供了有限的图像处理过滤器(算法)。性能加速是否适用于其它过滤器取决于过滤器复杂性和像素之间的相关性。不保证其它过滤器利用所述的技术也能提高性能。

下载 PDF 文档

要阅读本文的其余部分,请点击此处下载 PDF 文档(pdf 大小:1MB)

英特尔® 编译器英特尔® SSE 与英特尔® AVX 生成(SSE2、SSE3、SSE3_ATOM、SSSE3、SSE4.1、SSE4.2、AVX、AVX2)选项和特定处理器优化选项

$
0
0

在 11.1、12.0 和 12.1 编译器中哪些是 IA-32 和英特尔® 64 处理器的目标选项?
特定处理器优化选项主要有三类:

  1. 格式为 /arch:<code>(Windows*)(Linux* 或 Mac OS* X 上为 -m<code>)的特定处理器选项生成由 <code> 指定的专门的处理器代码。这些特定处理器选项生成的可执行文件,可以在支持此指令集的指定或较新的英特尔® 处理器和兼容的非英特尔® 处理器上运行。可执行文件可能具有这些处理器特有的优化特性,使用特定版本的英特尔® SIMD 流指令扩展 (SSE) 指令集和/或英特尔® 高级矢量扩展 (AVX) 指令集;在不支持相应指令集的较早处理器上,可能出现非法指令或类似错误。
  2. <code> 值可能的位置:

    AVX可能生成英特尔® AVX、英特尔® SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE4.2可能生成英特尔® SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE4.1可能生成英特尔® SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSSE3可能生成英特尔® SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE3可能生成英特尔® SSSE3、SSE2 和 SSE 指令。
    SSE2可能生成英特尔® SSE2 和 SSE 指令。/arch:SSE2 是 Windows* 的默认值,-msse2 是 Linux* 的默认值。
    IA32生成通用的 IA-32 兼容代码。只能与 /arch: 或 -m 交换结合使用。(仅 IA-32 编译器)。

  3. 格式为 /Qx<code>(Windows*)(Linux* 或 Mac OS* X 上为 -x<code>)的特定处理器选项生成由 <code> 指定的专门的处理器代码。由于特定处理器选项生成的可执行文件可能具有这些处理器特有的优化特性,且使用特定版本的 SIMD 流指令扩展 (SSE) 指令集和/或英特尔® 高级矢量扩展 (AVX) 指令集,因而只能在指定或较新的英特尔® 处理器上运行。此交换使某些优化无法在相应的交换 /arch:x<code> 或 -m<code> 中实现。运行时检查将被插入到生成的可执行文件中,如果在不兼容的处理器上运行,此文件将停止该应用。这将帮助您快速发现不用于所运行的处理器的程序,有可能避免出现非法指令错误。为使这一检查行为有效,包含主程序或动态库主函数的的源文件应该使用此选项进行编译。

  4. <code> 值可能的位置:

    CORE-AVX2可能为英特尔® 处理器生成英特尔® AVX2、英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。面向未来的英特尔处理器进行优化。
    CORE-AVX-I
    core-avx-i
    可能为英特尔® 处理器生成英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令,包括针对第三代英特尔® 酷睿™ 处理器的指令。针对第三代英特尔® 酷睿™ 处理器进行优化。
    AVX可能为英特尔® 处理器生成英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。针对第二代英特尔® 酷睿™ i7、i5 和 i3 处理器家族,以及英特尔® 至强® 处理器 E5 和 E3 家族进行优化。
    SSE4.2可能为英特尔® 处理器生成英特尔® SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。针对第二代英特尔® 酷睿™ i7、i5 和 i3 处理器家族,英特尔® 至强® 55XX、56XX 和 75XX 系列,以及英特尔® 至强® 处理器 E7 家族进行优化。
    SSE4.1可能为英特尔® 处理器生成英特尔® SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。针对 45 纳米高-k 新一代英特尔® 酷睿™ 微架构进行优化。
    SSSE3可能为英特尔® 处理器生成英特尔® SSSE3、SSE3、SSE2 和 SSE 指令。针对英特尔® 酷睿™ 微架构进行优化。-xssse3 是 Mac OS* X 上英特尔® 64 编译器的默认值。
    SSE3_ATOM 可能为英特尔® 处理器生成英特尔® SSSE3、SSE3、SSE2 和 SSE 指令。针对英特尔® 凌动™ 处理器家族和英特尔® 迅驰® 凌动™ 处理器技术进行优化。
    SSE3可能生成英特尔® SSSE3、SSE2 和 SSE 指令。针对增强的奔腾 M 处理器微架构和英特尔® Netburst 微架构进行优化。-xssse3 是 Mac OS* X 上 IA-32 编译器的默认值。
    SSE2 可能生成英特尔® SSE2 和 SSE 指令。针对英特尔® Netburst 微架构而优化。

  5. 格式为 /Qax<code>(Windows*)(Linux* 或 Mac OS* X 上为 -ax<code>)的处理器调度选项允许生成多个英特尔® 处理器代码路径。处理器调度技术执行执行时间检查,以确定应用运行的处理器,并针对该处理器使用最适合的代码路径。兼容的非英特尔处理器将使用经过优化的默认代码路径。上述第 1. 和 2. 条介绍的交换可用于修改经过优化的默认代码路径。

  6. <code> 值可能的位置:

    CORE-AVX2 可能为英特尔® 处理器生成英特尔® AVX2、英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    CORE-AVX-I
    core-avx-i
    可能为英特尔® 处理器生成英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令,包括针对第三代英特尔® 酷睿™ 处理器的指令。
    AVX可能为英特尔® 处理器生成英特尔® AVX、SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE4.2可能为英特尔® 处理器生成英特尔® SSE4.2、SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE4.1可能为英特尔® 处理器生成英特尔® SSE4.1、SSSE3、SSE3、SSE2 和 SSE 指令。
    SSSE3可能为英特尔® 处理器生成英特尔® SSSE3、SSE3、SSE2 和 SSE 指令。
    SSE3可能为英特尔® 处理器生成英特尔® SSE3、SSE2 和 SSE 指令。
    SSE2 可能为英特尔® 处理器生成英特尔® SSE2 和 SSE 指令。

哪个特定处理器选项最适合我的处理器?

CORE-AVX2未来的英特尔处理器
CORE-AVX-I 第三代英特尔® 酷睿™ i7 处理器
第三代英特尔® 酷睿™ i5 处理器
AVX第二代英特尔® 酷睿™ i7 处理器
第二代英特尔® 酷睿™ i5 处理器
第二代英特尔® 酷睿™ i3 处理器
英特尔® 至强® 处理器 E5 家族
英特尔® 至强® 处理器 E3 家族
SSE4.2上一代英特尔® 酷睿™ i7 处理器
上一代英特尔® 酷睿™ i5 处理器
上一代英特尔® 酷睿™ i3 处理器
英特尔® 至强® 55XX 系列
英特尔® 至强® 56XX 系列
英特尔® 至强® 75XX 系列
英特尔® 至强® 处理器 E7 家族
SSE4.1英特尔® 至强® 74XX 系列
四核英特尔® 至强 54XX、33XX 系列
双核英特尔® 至强 52XX、31XX 系列
英特尔® 酷睿™ 2 至尊 9XXX 系列
英特尔® 酷睿™ 2 四核 9XXX 系列
英特尔® 酷睿™ 2 双核 8XXX 系列
英特尔® 酷睿™ 2 双核 E7200
SSSE3四核英特尔® 至强® 73XX、53XX、32XX 系列
双核英特尔® 至强® 72XX、53XX、51XX、30XX 系列
英特尔® 酷睿™ 2 至尊 7XXX、6XXX 系列
英特尔® 酷睿™ 2 四核 6XXX 系列
英特尔® 酷睿™ 2 双核 7XXX(不包括 E7200)、6XXX、5XXX、4XXX 系列
英特尔® 酷睿™ 2 单核 2XXX 系列
英特尔® 奔腾® 双核处理器 E2XXX、T23XX 系列
SSE3_ATOM 英特尔® 凌动™ 处理器
SSE3双核英特尔® 至强® 70XX、71XX、50XX 系列
双核英特尔® 至强® 处理器(ULV 和 LV)1.66、2.0、2.16
双核英特尔® 至强® 2.8
支持 SSE3 指令集的英特尔® 至强® 处理器
英特尔® 酷睿™ 双核
英特尔® 酷睿™ 单核
英特尔® 奔腾® 双核处理器 T21XX、T20XX 系列
英特尔® 奔腾® 处理器至尊版
英特尔® 奔腾® D
支持 SSE3 指令集的英特尔® 奔腾® 4 处理器
SSE2(default)英特尔® 至强® 处理器
英特尔® 奔腾® 4 处理器
英特尔® 奔腾® M
IA32英特尔® 奔腾® III 处理器
英特尔® 奔腾® II 处理器
英特尔® 奔腾® 处理器


默认定位的是哪路处理器?

  • 在运行 Windows* 的 IA-32 系统上,默认开启 /arch:SSE2。在运行 Linux* 的 IA-32 系统上,默认开启 -msse2。生成的代码路径应在以下服务器上运行:支持 SSE2 的英特尔奔腾 4 和英特尔至强处理器,以及其他支持 SSE2 的较新版本英特尔处理器或兼容的非英特尔处理器。
  • 在运行 Mac OS* X 的 IA-32 系统上,默认开启 -xSSE3。编译器可能生成 SSE3、SSE2 和 SSE 指令,代码针对增强的奔腾 M 处理器微架构进行优化。
  • 在运行 Mac OS* X 的英特尔 64 系统上,默认开启 -xSSE3。编译器可能生成 SSSE3、SSE3、SSE2 和 SSE 指令,代码针对英特尔® 酷睿™ 微架构进行优化。

要定位不支持 SSE2 指令的较早 IA-32 系统(如基于英特尔® 奔腾® III 处理器系统),请使用交换 /arch:ia32 (Windows*) 或 -mia32 (Linux*)。

有关其他较早版本处理器定位选项的信息,以及这些选项与上述推荐的选项的关系,请参阅
/en-us/articles/ia-32-and-intel64-processor-targeting-overview


其他常见问题
(后续文章)

 

优化声明

英特尔编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2、SSE3 和 SSSE3 指令集以及其它优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于处理器的优化仅适用于英特尔微处理器。部分非针对英特尔微体系架构的优化也为英特尔微处理器保留了下来。如欲了解更多有关本声明所涉及的特定指令集的信息,请参阅适用产品的“用户和参考指南”。

声明版本 #20110804

英特尔® MKL 的新特性

$
0
0

英特尔®MKL 10.3 的新特性

英特尔 AVX 是英特尔处理器演进的下一步。英特尔 AVX 优化已扩展至更多 MKL 函数,以便在英特尔的未来架构中获得更出色的性能。

  • 概要统计库:一个使用最新统计进步的优化并行库,通过提供先进算法以增强统计计算的准确度和性能。

扩展的 MKL C 语言支持:LAPACK 的 C 界面PARDISO 中 C 格式基于 0 的索引阵列

面向 Windows 的动态界面库:已添加新的动态界面库,以改进 Windows 中的 C# 或 Java 链接。

VML 中的例程级别模式控制:用户现在可以借助各函数中的新变量,在 VLM 中为各函数单独控制或设置精确度。

英特尔® MKL 10.3 目录变更


英特尔® MKL 10.2 新特性

 

 

优化声明

英特尔编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2®、SSE3 和 SSSE3 指令集以及其它优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于处理器的优化仅适用于英特尔微处理器。部分非针对英特尔微体系架构的优化也为英特尔微处理器保留了下来。如欲了解更多有关本声明所涉及的特定指令集的信息,请参阅适用产品的《用户和参考指南》。

声明版本 #20110804

诊断信息 15532: 循环无法进行矢量化处理:编译时间不足妨碍了循环进行优化

$
0
0

产品版本: Intel(R) Visual Fortran 编译器 XE 15.0.0.070

原因:

使用 Visual Fortran 编译器的优化选项 ( -O2  -Qopt-report:2 )  时出现矢量化报告,表示编译时间不足妨碍了优化。

示例:

 

下面的示例将在优化报告中生成以下注释:

subroutine foo(a, n)

       implicit none
       integer, intent(in) :: n
       double precision, intent(inout) :: a(n)
       integer :: bar
       integer :: i

       i=0
 100   CONTINUE
       a(i)=0
       i=i+1
       if (i .lt. bar()) goto 100

  end subroutine foo

报告来源: 循环嵌套,矢量和自动并行优化 [循环、矢量、并行]

  循环开始  
      注释编号15532: 循环无法进行矢量化处理:编译时间限制阻拦循环进行优化 考虑使用 -O3。
 
  循环结束

解决方法:

使用 -O3 优化选项并参阅诊断信息 15523: 

GOTO 语句阻拦矢量化处理,因为无法计算循环迭代次数。

另请参阅:

矢量化循环要求

矢量化要点

矢量化和优化报告

返回英特尔 Fortran 的矢量化诊断列表

 

英特尔® 至强™ 处理器 E7 v3 助力提升 Java* 应用性能

$
0
0

背景

Java1, 2是一种编程语言,用于开发可运行于任意操作系统 (OS) 的应用。 为此,Java 应用必须编译成字节码,3后者不需要重新编译,可直接运行于任意 Java 虚拟机 (JVM) 4。 为在 Windows*,Linux* 等操作系统上运行 Java 应用,必须安装 Java Runtime Environment (JRE)7

基于 Java 的应用的优势在于,当底层的 Java Virtual Machine (JVM) 面向供应用运行的平台优化后,该应用无需更改代码或重新编译就可显著提升性能。

下文将探讨面向英特尔® 至强™ 处理器 E7 v3 的 JVM 所实现的性能改进,列举基于 Java 的应用 TYDIC* 在线收费系统 (OCS),并展示其性能如何基于英特尔® 至强™ 处理器 E7-8800 v3 得以增强。

面向英特尔® 至强™ 处理器 E7 v3 的 JVM 改进

英特尔与 Oracle 一直保持密切合作,以面向英特尔至强处理器 E7 v3 产品家族优化 JVM。 本节将列举(相比于前代英特尔至强处理器 (E7 v2))英特尔至强处理器 E7 v3 所具备的,有利于提升 Java 性能的几项新特性。

  • 更多内核,更大容量高速缓存:相比于譬如 E7 v2 等前代英特尔至强处理器,英特尔至强处理器 E7 v3 采用更多内核和更高容量的高速缓存。 例如,英特尔至强处理器 E7-8890 v3 采用 18 个内核和 45MB 高速缓存,而英特尔至强处理器 E7-4890 v2 采用 15 个内核和 37.5MB 高速缓存。
  • 更高的内存带宽:英特尔® 快速通道互联技术(英特尔® QPI)8和集成式内存控制器可提供快速的内核至内核以及内核至内存通信,从而显著提升了受内存限制的 Java 应用的性能。 此外,垃圾回收 (GC) 流程也得到了改进。 英特尔至强处理器 E7-4890 v2 的内存带宽和垃圾回收速度分别为 85GB/秒和 8GT/秒,相比之下,英特尔至强处理器 E7 v3 提升至了 102GB/秒和 9.6GT/秒。
  • 加速阵列和字符串操作:英特尔® 高级矢量扩展指令集 2(英特尔® AVX2) 提供了出色的矢量化优势,显著增强了阵列和字符换操作的性能。 所有版本的 JDK8 均支持英特尔 AVX2。9
  • • 硬件支持主要关注“事务内存”:10 包含锁争用现象的多线程 Java 应用将大大受益于英特尔® 交易同步扩展(英特尔® TSX)11。 当 -XX:+UseRTMLocking 选项在命令行内运行时,JVM 将自动使用该特性,从而提升 Java 应用性能。 JDK8u20 及未来版本均支持英特尔 TSX。

下一节我们将讨论,TYDIC OCS 在基于英特尔至强处理器 E7-8890 v3 的系统上运行时将如何受益。

TYDIC OCS

借助 OCS 软件,通信服务提供商能够根据服务的使用实时向客户收取费用。5

图 1 所示为 TYDIC 如何从在基于大型单片 RISC 的系统上运行的应用演进成采用基于分布式英特尔至强处理器的架构的应用。


图 1. 对比 RISC 与基于英特尔® 至强™ 处理器的 OCS 架构

全新架构包含三大趋势:

  • 将四路英特尔至强处理器 E7 平台作为‘选择节点’
  • 将 OCS 应用的架构重新设计为云架构:
    • 该应用承担着共享英特尔至强处理器 E7 平台的高可用性/正常运行时间的责任(不仅仅只依赖传统的 RISC 平台)。
    • 随着需求的增长,只需另外添加一个节点,该应用就可提供扩展方法,从而有效应对客户需求。
  • 通过跨英特尔至强处理器 E7 平台形成内存数据网格,以实现“内存 (in-memory)”,从而快速进行事务处理。

性能测试流程

为证明 OCS 应用在基于英特尔至强处理器 E7 v3 系统运行时如何获取优势,我们在两种平台上开展了测试。 一种系统配备英特尔® 至强™ 处理器 E7-8890 v3,另一种配置英特尔® 至强™ 处理器 E7-4890 v2。

该工作负载基于数据网格 6技术和纯内存计算,(所有业务数据均保存在内存之中!)因此可大大受益于英特尔至强处理器 E7 v3 平台的大容量内存和高带宽。 而且,内存计算能够通过消除 IO 瓶颈使 CPU 利用率达到完全饱和,并充分利用英特尔至强处理器 E7 v3 的更多内核。

注:本测试旨在对比 TYDIC OCS 在两种系统上运行时的性能,其中一种系统配备英特尔至强处理器 E7-8890 v3,另一种配备英特尔至强处理器 E7-4890 v2。 测试并非为了对比基于这些系统的 JVM 性能。

测试配置

配备英特尔® 至强™ 处理器 E7-8890 v3 的系统

  • 系统:预生产
  • 处理器:英特尔至强处理器 E7-8890 v3 @2.5GHz
  • 末级高速缓存:45MB
  • 每路处理器的内核数:18
  • 内存:128GB DDR4-1600MHz

配备英特尔® 至强™ 处理器 E7-4890 v2 的系统

  • 系统:预生产
  • 处理器:英特尔至强处理器 E7-4890 v2 @2.8GHz
  • 末级高速缓存:37.5MB
  • 每路处理器的内核数:15
  • 内存:128GB DDR3-1333MHz

操作系统:Red Hat* Enterprise Linux* 7

应用:OCS 2.1

测试结果

图 2 所示为配备两种版本的英特尔至强处理器的系统所得出的结果。 得益于英特尔至强处理器 E7-8890 v3 更快的 DDR4 内存、更多的内核以及演进的微体系结构,性能提升了 1.22 倍。


图 2. 英特尔® 至强™ 处理器 E7-8890 v3 与英特尔® 至强™ 处理器 E7-4890 v2 之间的对比

注: 在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下才能得到优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能。 上述任何要素的变动都有可能导致测试结果的变化。 请参考其它信息及性能测试(包括结合其它产品使用时的运行性能)以对目标产品进行全面评估。 更多信息敬请登录http://www.intel.com/performance

结论

JDK8u20 及其未来版本中的 JVM 通过优化,可有效利用英特尔至强处理器 E7 v3 的众多全新特性。 其更多内核、更快的内存以及具备的英特尔 AVX2 和英特尔 TSX 等特性能够支持运行于 JVM,但受内存限制,阵列或字符串操作繁重,或面临锁争用问题的 Java 应用轻松提升性能,无需更改代码或重新编译。

参考资料

[1] http://en.wikipedia.org/wiki/Java_%28programming_language%29

[2] https://java.com/en/download/faq/whatis_java.xml

[3] https://en.wikipedia.org/wiki/Java_bytecode

[4] http://en.wikipedia.org/wiki/Java_virtual_machine

[5] http://en.wikipedia.org/wiki/Online_charging_system

[6] http://en.wikipedia.org/wiki/Data_grid

[7] http://searchsoa.techtarget.com/definition/Java-Runtime-Environment

[8] http://www.intel.com/content/dam/doc/white-paper/quick-path-interconnect-introduction-paper.pdf

[9] https://en.wikipedia.org/wiki/Java_Development_Kit

[10] https://en.wikipedia.org/wiki/Transactional_Synchronization_Extensions

[11] https://software.intel.com/zh-cn/blogs/2012/02/07/transactional-synchronization-in-haswell

声明

本文档不代表英特尔公司或其它机构向任何人明确或隐含地授予任何知识产权。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的所有信息可随时更改,恕不另行通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文所述的产品和服务可能包含与宣称的规格不符的缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问www.intel.com/design/literature.htm

英特尔、Intel 标识、英特尔至强和 Intel Xeon 是英特尔在美国和/或其他国家的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2015 年版权所有。


基于英特尔® 架构加速金融应用

$
0
0

下载 PDF加快基于英特尔架构的金融应用 [PDF 575.55KB]

下载文件QuantLib_optimized_for_IA.tar.gz [TAR 522.48KB]

摘要:
 

文章加快基于 GPU 的金融应用通过 4 个 QuantLib 库金融工作负载对 GPU 和 CPU 进行了对比。 根据该文章报告,GPU 性能实现了显著提升,将运行单条线程的 Monte-Carlo* 工作负载的速度提升了高达 1,000 倍。 经过仔细调查我们发现,并行化方法不足以合理利用 CPU 提供的所有并行资源。 因此我们决定通过优化原始 CPU 代码,并基于最新 GPU/CPU 硬件重新运行测试,以开展深入的代码现代化性能分析。 分析结果与文章报告的数据大相径庭,在一些案例中,CPU 的性能甚至高于 GPU。

 

请查看 moderncode开发人员社区,了解更多信息。

Intel® System Studio 示例与教程

$
0
0

Intel® System Studio 是一款全面的集成工具套件,为开发人员提供了高级系统工具和技术,旨在推动加速交付下一代可靠的节能型高性能嵌入式设备和移动设备。

我们创建了一个展示 Intel System Studio 不同功能的示例列表,另外教程也将为您介绍应用中的功能如何使用。

下载或复制全部或任意部分的示例源代码表示您同意英特尔® 示例源代码许可协议的条款

示例

示例代码名称

描述

Hello World

这是一个简单的 "Hello World"示例,说明如何在命令行和 IDE 等不同的使用模式下,通过 Intel Compiler (ICC) for Windows*、Linux* Host 和 Yocto* Linux* Target 设置环境,构建嵌入式应用。

矩阵乘法

这是一个“矩阵乘法”示例,说明 Intel® System Studio 的不同功能,如 Intel® C/C++ Compiler、Intel® MKL、Intel® VTune Amplifier 和 Intel® Cilk Plus。

系统跟踪 – 示例跟踪

示例跟踪文件 (sampleTrace.tracecpt) 包含在这个系统系统调试器 NDA 包中。 这个示例跟踪文件来自真正的 Intel® Skylake 机器,包括 BIOS、CSME、TSCU 和全球错误包示例等多个跟踪包类型。 开始使用系统跟踪工具(eclipse 插件)调试系统问题之前,这一示例跟踪文件能够帮助您更加熟悉所有用户界面操作、系统跟踪工具提供的功能,如搜索关键词、打开新字段和导出部分记录等。

处理器跟踪示例

英特尔® 处理器跟踪是基于硬件的指令级低开销代码执行记录,提供有关过去指令流和交互式调试的深入洞察。

图像模糊和旋转

本教程演示如何:

  • 借助英特尔 IPP 过滤功能实施图像框模糊
  • 借助英特尔 IPP 仿射变形功能旋转图像
  • 设置环境,构建英特尔 IPP 应用
  • 编译和链接您的图像处理应用

 

平均滤波器(图像处理)

平均滤波器是图像处理领域的常用滤波器,主要用于除去特定图像中的任意噪声。 这一示例展示了如何使用 Intel® Cilk™ Plus 提高平均滤波器的性能。 从性能调整方面探索了线程和 SIMD 解决方案,并评估了它们在加速中的相应作用。

离散余弦变换(DCT)

离散余弦变换 (DCT) 和量化是 JPEG 压缩标准中的前两个步骤。 这一示例演示了如何实施 DCT 和量化阶段,借助 Intel® Cilk™ Plus 加快运行速度。

图像处理: 褐色滤光镜

褐色色调图像是一个鲜明的棕灰颜色的单色图像,当黑白底片可用时为照片提供一个独特的色调。 这一程序将位图文件中的每个像素转换为褐色色调。 这一示例展示了如何使用 Intel® Cilk™ Plus 提高褐色滤光镜的性能。 为了证明性能提升,您将使用一个把来自彩色图像的位图文件转换为褐色色调图像的程序。

VTune Amplifier for Systems 2016 - 嵌入式 Linux 目标的性能分析

这篇文章的目的概述如何使用 Intel(R) System Studio 2016 中的 VTune Amplifier for Systems 2016 进行针对嵌入式操作系统的远程性能分析。 主机将运行 Ubuntu* Linux 14.04 LTS,目标系统是通过 Intel(R) Common Core BSP (Intel-corei7-64) 构建、在 MinnowBoard Max(有一个双核 E3825 英特尔(R) 凌动(TM) 处理器)上运行的 Yocto* Project 1.8。

教程

教程演示的标题/链接

描述

使用面向嵌入式 Linux 系统的英特尔® C++ 编译器

英特尔® C++ 编译器也称为 icc,也一种高性能编译器,支持您面向基于 Linux* 的操作系统构建和优化您的 C/C++ 应用。 嵌入式系统开发在大多数情况下都是跨平台开发。 应用开发通常需要交叉编译,而交叉编译需要一个主机编译系统和一个目标嵌入式系统。 英特尔® C++ 编译器也完全支持跨平台编译。

Intel® VTune™ Amplifier for Systems 使用模式

Intel® VTune™ Amplifier for Systems 是一种软件性能分析工具,供用户在嵌入式系统和移动系统上开发串行和多线程应用。 根据您的开发环境和目标环境,VTune Amplifier 支持面向不同目标系统的多种使用模式。 在本文中,我们将介绍 Vtune Amplifier 使用模式和不同目标系统的推荐模式。

Intel® System Studio 的信号处理使用 – 英特尔® MKL 与 英特尔® IPP

采用性能库可以很好地简化和统一数据密集型任务的计算执行流程,从而最大限度降低数据流时序问题和 heisenbug 的风险。 在文本中,我们将介绍可在 Intel® System Studio 中用于信号处理的两个库。

使用 OpenPCD* 调试 基于 Intel® Quark SoC 的目标平台

本教程将帮助您了解如何为基于 Intel Quark 的目标系统设置基于 OpenOCD* 的连接器,以及如何使用 Intel System Studio 调试系统软件。

英特尔® 至强™ 处理器 E5-2600 V3 产品家族技术概述

$
0
0

目录

1. 要点综述
2. 简介
3.英特尔至强处理器 E5-2600 V3 产品家族增强性能。
  3.1 英特尔® 高级矢量扩展指令集 2 (英特尔® AVX2)指令
  3.2 Haswell 新指令 (HNI)
  3.3 支持 DDR4 内存
  3.4 电源改进
4. Grantley 平台改进
  4.1 英特尔® C610 系列芯片组 (Wellsburg)
  4.2 虚拟化特性
  4.3 全新安全特性
  4.4 英特尔® 节点管理器 3.0
5. 结论
作者介绍

1. 要点综述

英特尔® 至强™ 处理器 E5-2600 V3 产品家族(代号 “Haswell EP”)是基于英特尔最新微架构的一款双插槽平台。 它是基于 22 纳米制程技术的新 “TOCK” 。 该产品可为数据中心带来更多功能:更多的内核、更大的内存和更高的带宽。 因此,与上一代 “Ivy Bridge EP” 相比,基于英特尔至强处理器 E5-2600 V3 产品家族的平台的性能将提升 33%1。 该平台新增了许多特性(硬件和软件)。  在硬件端,将提供更多的内核和更高的内存带宽、DDR4 内存支持、功耗增强特性、虚拟化增强特性和一些安全增强特性(系统管理模式外部调用陷阱),它们能够提高显著提升应用性能,而无需开发人员提供任何支持。  在硬件端有 HNI 和 AVX2。  这些软件特性需要开发人员提供应用支持。

2. 简介

英特尔至强处理器 E5-2600 V3 产品家族基于 Haswell 微架构,它较之 Ivy Bridge EP 微架构在多方面进行了增强 (http://software.intel.com/zh-cn/articles/intel-xeon-processor-e5-2600-v2-product-family-technical-overview)。  支持英特尔至强处理器 E5-2600 V3 产品家族的平台名为 “Grantley”。 本文介绍了较之英特尔至强处理器 E5-2600 V2 产品家族,英特尔至强处理器 E5-2600 V3 产品家族中新增的特性。 每一章节都对开发人员利用新特性改进应用性能和安全性应执行的操作进行了介绍。

3. 英特尔至强处理器 E5-2600 V3 产品家族增强性能

Intel® Xeon® processor E5-2600 V3 product family

图 1: 英特尔® 至强® 处理器 E5 2600 V3 产品家族概述

英特尔至强处理器 E5-2600 V3 产品家族中随附的新特性包括:

  1. 英特尔® 高级矢量扩展指令集 2 (英特尔® AVX2)指令
  2. Haswell 新指令 (HNI)
  3. 支持 DDR4 内存
  4. 电源管理特性改进

图 1 概要展示了英特尔至强处理器 E5-2600 V3 产品家族微架构。 该产品家族的所有处理器都有 18 个内核(上一代中有 12 个内核),这提供了更高的计算能力。 它们还可提供更大的高速缓存(高频 SKU — 英特尔® 至强™ E5-2699 v3 拥有 45 MB(Ivy Bridge 仅为 30 MB))和更高的内存带宽。

表 1. 英特尔® 至强™ 处理器 E5–2600 V3 产品家族与英特尔® 至强™ 处理器 E5–2600 V2 产品家族之比较

Feature List

接下来本文将介绍该产品家族中的主要增强特性。

  3.1 英特尔® 高级矢量扩展指令集 2 (英特尔® AVX2)指令

借助英特尔 AVX,所有浮点矢量指令从 128 位扩展到 256 位。 英特尔 AVX 2 还将整数矢量指令扩展到 256 位。 英特尔 AVX 2 与英特尔 AVX 使用相同的 256 位 YMM 寄存器。 AVX2 指令可为高性能计算 (HPC) 的应用、数据库、音频和视频应用带来优势。 AVX 2 指令包括融合乘加 (FMA)、Gather、Shifts 和 Permute 指令。

融合乘加 (FMA) 指令仅使用一次凑整即可计算 ±(a×b)±c。不会对 .axb 中间结果进行凑整,因此比 MUL 和 ADD 指令更准确。 FMA 可以提升许多浮点计算的性能和准确性,如矩阵乘法、点积和多项式求值。 借助 256 位,我们能够执行 8 次单精度和 4 次双精度 FMA 操作。 由于 FMA 将 2 次操作合而为一,每秒浮点操作 (FLOPS) 提升;此外,由于 Haswell 有 2 个 FMA 设备,峰值 FLOPS 翻倍增长。

Gather 指令可将稀疏元素加载为一个矢量。 在一次操作中,它能够将 8 个单精度 (Dword) 或 4 个双精度 (Qword) 数据元素收集到一个矢量寄存器中。 有一个基本地址用来指明内存中的数据结构。 索引(偏移)可以显示每个元素相对于基本地址发生的偏移。 屏蔽寄存器可以追踪需要收集哪个元素。 当屏蔽寄存器全部为 0 时,Gather 完成。 gather 指令可支持工作负载矢量化,由于各种原因,以前无法对工作负载进行矢量化处理。

其他英特尔 AVX2 的新操作包括整数版 permute 指令、新 Broadcasts 指令和 Blend 指令。

  3.2 Haswell 新指令 (HNI)

Haswell 新指令包括 4 个 Crypto 指令,用于加速公共密钥和 SHA 加密算法;以及 12 个(位操作)指令,用于加速压缩或信号处理算法。 位操作指令可以执行任意位字段的操作,引导并追踪零位计数,追踪置位操作、改进旋转以及任意精度的乘运算。 它们可以加速执行位字段提取和打包的算法、以位精度编码的数据处理(压缩算法通用编码)、任意精度乘运算和散列。

如要使用 HNI,您需要使用更新的编译器,如下表所示:
Various Compiler support options

表 2: 多种适用于新指令的编译器支持选项

关于英特尔® C++ 编译器的更多信息,请访问: https://software.intel.com/zh-cn/intel-parallel-studio-xe

  3.3 支持 DDR4 内存

英特尔® 至强™ 处理器 E5–2600 V3 产品家族支持 DDR3 和 DDR4 内存。 相比 DDR3,DDR4 能够节省 35% 的功耗(每通道 2 个 DIMM),且带宽性能提升 50%1

  3.4 电源改进

英特尔® 至强™ 处理器 E5–2600 V3 产品家族的电源改进包括:

  • Per core P-states (PCPS)
    • 每个内核可以变成为操作系统 (OS) 要求的 P 状态
  • 非内核频率调整 (UFS)
    • 非内核频率的控制独立于内核频率。
    • 通过将功率用到需求最高的地方来优化性能
  • 更快的 C 状态
    • 当从 C3 或 C6 状态唤醒内核时,将需要一段时间。  这一时间在 HSX 上更快。  这减少了执行 C 状态的开销。
  • 更低的闲置功率

联系您的操作系统 (OS) 提供商,具体了解操作系统支持哪些功能。

4. Grantley 平台改进

Grantley 平台中的新功能包括:

  • 英特尔® C610 系列芯片组 (Wellsburg)
  • 全新虚拟化特性
  • 全新安全特性
  • 英特尔® 节点管理器 3.0

  4.1 英特尔® C610 系列芯片组 (Wellsburg)

Grantley 平台配备了英特尔® C610 系列芯片组 (Wellsburg),而上一代 Romley 平台配备了英特尔® C600 芯片组 (Patsburg) 。 C610 芯片组与 C600 相比,TDP 和每软件包的平均功率得到提升。

表 3 对 C600 和 C610 芯片组的特性进行了比较。

表 3 Patsburg 和 Wellsburg 特性比较

  4.2 虚拟化特性

Grantley 平台的虚拟化特性改进包括:

嵌套虚拟化可帮助根虚拟机监视器 (VMM) 支持客户机 VMM。 但是,额外虚拟机 (VM) 退出可能会影响性能。 VMCS 阴影可将客户机 VMM VMREAD/VMWRITE 导向 VMCS 阴影结构。 这可减少嵌套导致的 VM 退出。 VMCS 阴影可降低虚拟化延迟,从而提高效率


图 2: VMCS 阴影

该特性要求启用 VMM。 联系您的 VM 提供商,了解其何时提供该特性。

高速缓存监控技术(又称“相邻用户争用”管理)可提供末级高速缓存占用监控。 这支持 VMM 确认单个应用或 VM 级别的高速缓存占用。 借助该信息,虚拟化软件能够更好地决定如何调度和迁移工作负载。


图 3: Grantley 平台上的高速缓存监控

该特性要求启用 VMM。 联系您的 VM 提供商,了解其何时提供该特性。

在上一代 Romley 平台中,访问和“脏” 位(A/D 位)在 VMM 中进行仿真,访问它们会导致 VM 退出。 Grantley 在硬件中部署了 EPT A/D 位以减少 VM 退出。 这支持虚拟机实时、有效迁移,并支持容错。

EPT A/D in HW

图 4: 硬件中的 EPT A/D

该特性要求启用 VMM。 联系您的 VM 提供商,了解其何时提供该特性。

由于需要处理特权指令,在虚拟化来回转换时(从 VM “退出”到 VMM,从 VMM “进入” VM)性能开销会增加。   历代平台不断努力减少转换时间。 Grantley 进一步减少了 VMM 开销,并提升了虚拟化性能。

  1. 虚拟机控制结构(VMCS)阴影
  2. 高速缓存监控技术 (CMT)
  3. 扩展页表 (EPT) 访问/“脏” (A/D) 位
  4. VT-X 延迟减少

4.3 全新安全特性

Grantley 平台的安全特性改进包括:

  1. 系统管理模式 (SMM) 外部调用陷阱 (SECT)
    系统管理模式 (SMM) 是一款能够暂停所有正常执行(包括操作系统),并以优先模式执行特定的单个软件(通常为固件或硬件辅助调试器)的操作模式。 出现 SMI (系统管理中断)后,可输入 SMM 运行处理程序代码。 没有 SMM 外部调用陷阱 (SECT),SMI 处理程序可能会运行用户内存中可能是恶意代码的代码。 借助 SECT,SMI 处理程序无法调用用户内存中的代码。
    SIMM external call trap
    图 5: SIMM 外部调用陷阱

    启用该功能需要 BIOS 级别支持。
     
  2. 一般加密辅助 — AVX2、第四代 ALU、面向散列的 RORX

    AVX2 (256 位整数、更出色的位操作、Permute 粒度)和第四代 ALU (算法和逻辑单元)能够加速所有加密算法。 RORX 可以加速散列算法。 请参阅英特尔® 架构指令集扩展编程参考,了解关于该指令的更多信息

  3. 非对称加密辅助 — 适用于公共密钥的 MULX

    新指令 (MULX) 可以改进非对称加密并减少加密挑战。 请参阅英特尔® 架构指令集扩展编程参数,或关于该指令的其他信息

  4. 对称加密辅助 – AES-NI 优化
    Grantley 包括面向对称加密(英特尔® AES-NI 等)的增强特性和扩展。 请参阅本文,了解更多有关 AES-NI 及如何使用它的信息。
     
  5. PCH-ME 数字随机数生成器 (DRNG)

    管理引擎 (ME) 是平台架构中的一款独立的自动控制器。 ME 要求安全性较高的方法(鉴于其自动特性)以及访问低级别平台机制的权限。 如要最大限度地确保平台的安全性,须为 ME 提供高质量的随机数源。 PCH-ME DRNG 技术可提供真实的熵,并生成较难预测的随机数以便 ME 用来加密,这些随机数与其他系统资源相隔离。

4.4 英特尔® 节点管理器 3.0

Grantley 配备了最新版的英特尔® 节点管理器 — 3.0。 节点管理器 3.0 的改进包括:

  • 预测功率极限
    • 当系统功耗接近极限时,按计划出现功耗瓶颈
  • 在启动过程中强制执行功耗限制
    • 无需复杂的 IT 流程或禁用内核仅可控制“启动峰值”
  • 针对英特尔® 至强融核™ 协处理器的功耗管理
    • 为英特尔至强融核协处理器域及平台其他部分区分电源限制和控制
  • 节点管理器电源散热实用程序 (Power Thermal Utility, PTU)
    • 为 CPU 和内存域建立主要的电源特征值
    • 作为固件而提供

请访问该链接,了解关于节点管理器的更多详细信息。

5. 结论

概括而言,采用 Grantley 平台的英特尔至强处理器 E5-2600 V3 产品家族可提供多种全新及改进特性,从而能够显著提升您在企业平台上的性能和功耗体验。

关于作者

Sree Syamalakumari 现任英特尔公司软件和服务事业部的软件工程师。 Sree 拥有俄亥俄州代顿市莱特州立大学的计算机工程硕士学位。

评估使用 HEP 工作负载的多核平台的能效和性能

$
0
0

在摩尔定律的推动下,芯片行业一直致力于在芯片中集成更多的晶体管,导致处理器设计变得更加复杂。 发展的领域包括核心数量、执行端口、矢量单元、Uncore 架构和最终的指令集。 由于复杂性日益增加,访问共享内存成为主要的限制因素,这使得向内核传送数据成为巨大的挑战。 另一方面,整个行业对能效极为重视,这有助于实现电源感知型计算,并降低数据中心架构的复杂性。 在本文中,我们将尝试探讨这些趋势,并介绍使用英特尔® 至强™ E5 v3(代号 Haswell-EP)处理器产品家族和高度可扩展的高能物理学 (HEP) 工作负载进行实验的结果。

 

提升 OpenSSL 性能

$
0
0

目录

摘要
OpenSSL 概述
      什么是 SSL/TLS
      什么是 OpenSSL
      OpenSSL 1.0.2 加密改进的目的
OpenSSL 1.0.2 的关键部分
      函数拼接
      将多缓冲应用于 OpenSSL
系统配置与实验设置
      速度测试
性能
      AES 结果
      公共密钥加密结果
      拼接结果
      多缓冲结果
作者
撰稿人
结论
参考资料

 

摘要 


SSL/TLS 协议涉及两个计算密集型加密阶段:会话初始化和批量数据传输。 OpenSSL 1.0.2 推出了一套完善的加密函数增强功能,比如不同模式的 AES、SHA1、SHA256、SHA512 散列函数(面向批量数据传输),以及 RSA、DSA 和 ECC 等公共密钥密码(面向会话初始化)。 这类优化主要面向以 32 位和 64 位模式运行的英特尔® 酷睿™ 处理器和英特尔® 凌动™ 处理器。

OpenSSL [1] 是一种领先的开源实施方法,主要针对加密函数和要求使用 SSL/TLS [2] 协议的应用的 go-to 资源库。 本文结果主要面向常用于 SSL/TLS 会话初始化/握手和批量数据传输阶段的加密函数而提供。

 

OpenSSL 概述 


什么是 SSL/TLS 

TLS(传输层安全性) [2] 及其前身 SSL(安全套接层)是用于提供安全网络通信的加密协议。

该协议支持应用通过网络进行通信,同时防止信息被窃听和篡改。即第三方无法读取传输内容,也无法在接收端检测不到的情况下修改传输内容。

协议运行过程包含两个阶段。 第一个阶段是会话初始化。 服务器和客户端通过协商,选择适用于加密与验证的密码套件,以及共享密钥。 第二个阶段是传输批量数据。 协议采用数据包加密方法以确保第三方无法读取数据包的内容。 他们根据数据的加密散列表使用消息验证代码 (MAC),以确保数据在传输过程中不被修改。

在会话初始化阶段,客户端必须在生成共享密钥之前采用公共密钥加密方法向服务器传送私人消息。 最常用的公共密钥加密方法是基于模幂运算的 RAS。 模幂运算是一种计算密集型运算,占据了绝大多数的会话初始化周期。 模幂运算实施速度越快,会话初始化成本越低。

在 SSL 下,传输的批量数据被分成若干个最大为 16KB 的记录(面向 SSLv3 和 TLSv1)。

SSL Computations of Cipher and MAC

图 1: Cipher 和 MAC 的 SSL 运算

添加标头后,使用加密散列函数通过标头和数据对消息验证代码 (MAC) 进行计算。 MAC 附加在消息末端,这样消息就填充完毕。 除标头外,其他内容均通过选定的密码进行加密。

这里的关键是所有批量数据缓冲都适用两种算法:加密和验证。 在许多情况下,这两种算法可以拼接 [3] 在一起,以提升整体性能。 许多密码套件(比如 GCM)定义了加密+验证组合模式;在这种情况下,拼接运算会变得更加简单。

什么是 OpenSSL 

OpenSSL [1] 是一种针对 SSL 和 TLS 协议的开源实施方法,广泛用于许多应用和大型企业。

对于这些企业来说,OpenSSL 实施过程中最具吸引力的部分是服务器(每秒)可处理的连接数量,因为这直接代表了需用于服务客户群的服务器数量。 这种最大限度地增加连接数量的方法旨在最大限度地降低每次连接的成本,这可通过最大限度地降低会话初始化和会话数据传输的成本来实现。

OpenSSL 1.0.2 加密改进的目的 

某些针对加密优化的 OpenSSL 项目旨在:

  1. 扩展 OpenSSL 软件架构以支持多缓冲处理技术,从而发挥处理器中 SIMD 架构的最大性能。
  2. 使用高度优化的拼接算法提供市场领先的 SSL/TLS 性能。
  3. 扩展加密空间内的 SIMD 利用率(例如基于英特尔® 流式单指令多数据扩展(英特尔® SSE) 的 SHA2 实施)。
  4. 将英特尔® 高级矢量扩展指令集 2(英特尔® AVX2)用于多种加密算法(比如 RSA 和 SHA)。
  5. 在任何情况下都可使用新指令 MULX、ADCX、ADOX、RORX 和 RDSEED 发挥算法的最大性能。
  6. SSL/TLS 有效负载处理性能权衡应偏向低于约 1400 字节的有效负载。
  7. 将所有功能集成于 OpenSSL 1.0.x 及未来的 1.1.x 代码行,以确保使用现有 OpenSSL 接口的应用能够自动使用这些功能,无需进行其他初始化。

 

OpenSSL 1.0.2 的关键部分 


OpenSSL 1.0.2 中的部分关键加密优化包括:

  • 多缓冲 [4] 支持 AES [128|256] CBC 加密
  • 多缓冲支持 [SHA-1|SHA-256] 利用架构特性 [英特尔 SSE | 英特尔 AVX | 英特尔 AVX2-BMI2]
  • 单缓冲支持“拼接” AES [128|256] CBC [SHA-1|SHA-256] 利用架构特性 [英特尔 SSE | 英特尔AVX | 英特尔 AVX2]
    • AES-128-CBC-Encrypt-SHA-1-AVX2-BMI2
    • AES-256-CBC-Encrypt-SHA-1-AVX2-BMI2
    • AES-128-CBC-Encrypt-SHA-256-SSE
    • AES-256-CBC-Encrypt-SHA-256-SSE
    • AES-128-CBC-Encrypt-SHA-256-AVX
    • AES-256-CBC-Encrypt-SHA-256-AVX
    • AES-128-CBC-Encrypt-SHA-256-AVX2-BMI2
    • AES-256-CBC-Encrypt-SHA-256-AVX2-BMI2
  • 单缓冲支持“拼接” AES [128|256] GCM
  • 单缓冲 SHA-1 性能增强功能利用英特尔 AVX2 和 BMI2
  • 单缓冲 SHA-2 套件 SHA[224|256|384|512] 性能增强功能利用 [英特尔 SSE | 英特尔 AVX | 英特尔 AVX2-BMI2] [5]
  • RSA 和 DSA(密钥大小 >= 1024) 支持使用 [传统 | MULX | ADCX – ADOX] 指令 [6]
  • ECC – ECDH 和 ECDSA [MULX | ADCX – ADOX]
  • 英特尔® 安全散列算法扩展指令集(英特尔® SHA 扩展指令集)新指令 [7]

RSA/DSA/ECC 主要针对会话初始化阶段。 其他的旨在提升批量数据传输阶段的性能。 多缓冲实施可实现最大程度的加速,但目前仅面向加密流任务而设计。

通过函数拼接实施的算法对的选择以目前及未来最常用的密码套件为基础。 在无法采用函数拼接的场景下,对单个加密和验证函数进行优化。

函数拼接 

函数拼接技术用于优化通常以组合形式,但有序运行的两种算法,并精巧地将运算拼接在一起,以最大限度地利用计算资源。 这部分内容仅粗略介绍拼接技术。 更多详情请见 [3]。

函数拼接以超精细的方式交错传送每种算法的指令,以确保两种算法能够同时执行。 其优势在于,(由于数据相关性或指令延迟)执行某种算法时处于闲置状态的执行单元可用来执行另一种算法的指令,反之亦然 [3]。

将多缓冲应用于 OpenSSL  

多缓冲 [4] 是一种面向加密算法(比如散列和加密)能够高效、并行处理多个独立数据缓冲的方法。 同时处理多个缓冲可显著提升性能 — 无论是在代码能够利用 SIMD(英特尔 SSE 英特尔 AVX)指令(比如英特尔 SHA 扩展指令集)的情况下,还是在无法利用(例如使用英特尔® 高级加密标准新指令(英特尔® AES-NI)进行 AES CBC 加密)的情况下。

Multi-Buffer Processing

Multi-Buffer Processing

图 2: 多缓冲处理

多缓冲通常需要采用调度器,后者能够以最低的性能开销处理多个大小不同的数据缓冲,为此我们已找到了很好的解决方法。 然而,将多缓冲应用于 OpenSSL 时,我们的主要挑战和关键问题是如何将多缓冲集成于以串行方式设计的同步应用/框架。 为此,我们在加密期间将记录分成更小但大小相等的子记录。 但这种方法不适用于解密流。

 

系统配置和测试设置 


本部分提供的性能结果是基于三路英特尔酷睿处理器和双路英特尔凌动处理器的测量结果。 系统包括:

  1. 英特尔® 酷睿™ i7-3770 处理器 @ 3.4 GHz        (代号 “Ivy Bridge (IVB)”)
  2. 英特尔® 酷睿™ i5-4250U 处理器 @ 1.30 GHz     (代号 “Haswell (HSW)”)
  3. 英特尔® 酷睿™ i5-5200U 处理器 @ 2.20 GHz     (代号 “Broadwell (BDW)”)
  4. 英特尔® 凌动™ 处理器 N450 @ 1.66GHz          (代号 “Bonnell (BNL)”)
  5. 英特尔® 凌动™ 处理器 N2810 @ 2.00GHz         (代号 “Silvermont (SLM)”)

对于这三路英特尔酷睿处理器,这些测试运行于单个内核,且英特尔® 睿频加速技术关闭,而英特尔® 超线程技术(英特尔® HT 技术)开启。 请注意,英特尔酷睿 i5-5200U 处理器默认为以“省电”模式启动,并以 800 MHz 的速度运行这些测试。 不过所有测试结果均以周期形式提供,以准确呈现微体系架构的功能,并消除所有频率差异。

速度测试 

针对性能测试运行 OpenSSL ‘速度’基准测试。 部分示例命令行为:

./bin/64/openssl speed -evp aes-128-gcm

./bin/64/openssl speed -decrypt -evp aes-128-gcm

./bin/64/openssl speed -evp aes-128-cbc-hmac-sha1

./bin/64/openssl speed -decrypt -evp aes-128-cbc-hmac-sha1

./bin/64/openssl speed -mb -evp aes-128-cbc-hmac-sha1

请注意,“-mb” 切换为新命令,并已完成添加,以便运行多缓冲性能测试。

 

性能 


性能结果已经过标准化处理,在大多数情况下可转化成被处理数据的'每字节周期' (CPB) 。 CPB 是衡量加密算法效率的标准指标。

下图展示了针对 32 位和 64 位代码的性能。

注:性能测试和等级评定均使用特定的计算机系统和/或组件进行测量,这些测试反映了英特尔产品的大致性能。 任何系统硬件、软件的设计或配置的不同均可能影响实际性能。 购买者应进行多方咨询,以评估他们考虑购买的系统或组件的性能。 如欲了解有关性能测试和英特尔产品性能的更多信息,请访问: http://www.intel.com/performance/resources/benchmark_limitations.htm

AES 结果: 

AES Encrypt (Intel® Core™ processors)

图 3: AES 加密(英特尔® 酷睿™ 处理器)

IBV 至 HSW 的 AES-CBC 加密性能提升得益于 AESENC[LAST] 和 AESDEC[LAST] 指令中 1 周期的延迟降低。

IBV 至 HSW 的 AES-GCM 性能提升得益于英特尔 AVX 和 PCLMULQDQ 微体系架构的增强,而从 HSW 至 BDW 的性能提升则得益于 PCLMULQDQ 微体系架构的进一步增强。

AES Decrypt (Intel® Core™ processors)

图 4: AES 解密(英特尔® 酷睿™ 处理器)

大多数常见 AES 解密模式受制于吞吐量,而非延迟。 我们实施并行 AES-CBC 解密一次性处理 6 个数据块。

AES Encrypt (Intel® Atom™ processors)

图 5: AES 加密(英特尔® 凌动™ 处理器)

SLM 引入了 AES 和 PCLMULQDQ 指令,大大提高了 CBC 和 GCM 模式的速度。

AES Decrypt (Intel® Atom™ processors)

图 6: AES 解密(英特尔® 凌动™ 处理器)

公共密钥加密结果 

Public Key Cryptography (Intel® Core™ processors)

图 7: 公共密钥加密(英特尔® 酷睿™ 处理器)

基于 RAS 实现 IVB 提升主要得益于算法优化。

HSW RSA2048 属于个例,因为其性能提升得益于英特尔 AVX2 的实施。 其他性能提升则得益于标量代码调优/算法增强。

在 BDW 上添加 MULX/ADOX/ADCX(LIA 指令)后,通过 HSW 实现了显著的性能提升。

我们将通用代码添加至 Montgomery 相乘函数,因此它能够跨所有 RSA 大小、DSA、DH 和 ECDH 进行扩展。

Public Key Encryption 32-bit

Public Key Encryption 64-bit

图 8: 公共密钥加密(英特尔® 凌动™ 处理器)

基于 SLM 时,得益于乱序执行,实现了架构标量增强。

拼接结果 

AES-CBC-HMAC-SHA (Encrypt) Cycles/Byte

图 9: AES-CBC-HMAC-SHA (加密)周期/字节

AES-CBC-HMAC-SHA (Decrypt) Cycles/Byte

图 10: AES-CBC-HMAC-SHA (解密)周期/字节

IVB 至 HSW 性能提升主要得益于使用英特尔 AXV2 代码。

在解密情况下,AES 指令延迟增强并未实现性能提升,这是因为结果受到了 SHA 的约束。

由于扩展的寄存器组,拼接密码仅用于 64 位增强。 In v1.0.1 拼接密码仅支持解密。

多缓冲结果 

Multi-Buffer AES-CBC-HMAC-SHA (Encrypt) Cycles/Byte

图 11: 多缓冲 AES-CBC-HMAC-SHA (加密)周期/字节

Multi-Buffer Speedup over Stitched

图 12: 通过拼接实现多缓冲加速

 

作者 


Vinodh Gopal、Sean Gulley 和 Wajdi Feghali 是数据中心事业部的架构师,主要关注领域为与加密和压缩相关的软硬件特性。

Ilya Albrekht 和 Dan Zimmerman 是应用工程师,主要针对加密项目和资源库开展性能优化工作。

撰稿人 


感谢 OpenSSL 软件基金会的 Andy Polyakov 和 Steve Marquess,以及英特尔 Max Locktyukhin、John Mechalas 和 Shay Gueron 对本文所作出的贡献。

结论 


本文论述了 OpenSSL 1.0.2 关于提升加密性能的目的以及所具备的主要特性。 充分利用处理器的架构特性(比如 SIMD 和新指令),并结合创新型软件技术(比如函数拼接和多缓冲),可显著提升性能(例如,将多缓冲性能提升 3 倍)。

参考资料 


[1] OpenSSL: http://www.openssl.org/

[2] TLS 协议 http://www.ietf.org/rfc/rfc2246.txt

[3] 通过函数拼接在英特尔® 架构处理器上快速进行加密计算 http://www.intel.com/content/www/cn/zh/intelligent-systems/wireless-infrastructure/cryptographic-computation-architecture-function-stitching-paper.html

[4] 并行处理多个缓冲以提升英特尔® 架构处理器的性能 - http://www.intel.com/content/www/cn/zh/communications/communications-ia-multi-buffer-paper.html

[5] 基于英特尔® 架构处理器的快速 SHA-256 实施 - http://www.intel.com/content/www/us/en/intelligent-systems/intel-technology/sha-256-implementations-paper.html

[6] 新指令支持英特尔® 架构处理器执行大型整数算法

http://www.intel.com/content/www/us-en/intelligent-systems/intel-technology/ia-large-integer-arithmetic-paper.html

[7] 英特尔® SHA 扩展指令集新指令支持英特尔® 架构处理器执行安全散列算法

https://software.intel.com/zh-cn/articles/intel-sha-extensions 

Vectorization Advisor 助您一臂之力

$
0
0

Vectorization Advisor 助您一臂之力

如果您还未尝试过使用全新 Vectorization Advisor,那么强烈推荐您阅读一下这篇故事,了解它如何为客户提供巨大的帮助。它就像一位值得信赖的朋友,为您检查代码,并根据实际情况提供建议。本文还包含了有关该工具的用户反馈:“按照 advisor 的输出操作可显著提高速度,这款工具真的令我折服!”

飞机表面、喷墨打印,和分离名牌新款洗发露中使用的水油混合体或蛋白溶液,这三者之间的共同点是什么? 计算化学家会告诉您,要想推动这些领域的发展,通常需要执行凝聚态物质介观模拟。 “介观” 指此类模拟必须涉及体积稍大于原子的物体的质量,而 “凝聚态物质” 意味着您很可能要为液态或固态建模。

为满足与介观相关的各种科学工业要求,英国科学与技术设施理事会达斯伯里实验室 (STFC Daresbury Laboratory) 的研究科学家开发了一种名为 “DL_MESO” 的介观模拟程序包。 欧洲工业界的一些企业(比如 UnileverSyngentaInfineum)采用了 DL_MESO,他们使用介观模拟推导出配制洗发露、洗衣粉、农药或石油添加剂的最佳配方【计算机辅助配方 (CAF)】。

Figure 1. Visualization of the 3D_PhaseSeparation benchmark.

图 1. 3D_PhaseSeparation 基准测试虚拟化

计算机辅助配方模拟流程通常费时费力,且浪费资源。因此从一开始,达斯伯里实验室的专家就对 DL_MESO 面向现代平台的性能感知设计和优化功能非常感兴趣;这也可以解释 DL_MESO 为什么能够成为 Hartree 和英特尔开展的英特尔® 并行计算中心(英特尔® PCC)合作项目之一1。英特尔 PCC 项目致力于使用最新技术在最新系统上实现代码现代化。 就这一点而论,旨在采用新技术,为代码现代化提供帮助的 DL_MESO 自然而然地契合了英特尔 PCC 项目。

DL_MESO 工程师充分利用了 2015 年年初发布的早期预发布版 Vectorization Advisor 分析工具(产品版 Vectorization Advisor 现已成为英特尔® Parallel Studio XE 2016英特尔® Advisor 2016的一部分。)

DL_MESO 开发人员计划充分发挥现代英特尔平台的矢量并行化功能,这进一步提高了他们使用新技术的兴趣。 就多核英特尔® 至强™ 处理器或众核英特尔® 至强融核™ 平台而言,代码只有充分利用 CPU 并行化的两个层级(多核并行化和矢量数据并行化),才能达到较高的性能。 相比于未经矢量化代码,借助 512 位宽 SIMD 指令实现高效矢量化的代码,从理论上来说,可将面向双精度浮点计算的性能提高 8 倍(或将面向单精度浮点计算的性能提高 16 倍)。 DL_MESO 开发人员不希望浪费如此高的性能。

在本文中,我们将介绍达斯伯里实验室的计算科学家 Michael Seaton 和 Luke Mason 如何使用 Vectorization Advisor 分析 DL_MESO Lattice Boltzmann Equation 代码2。 Hartree 的一位首席开发人员对使用 Vectorization Advisor 所带来的成效惊叹不已,他热情地写到:“按照 advisor 的输出操作可显著提高速度,这款工具真的令我折服!”

在全新多核英特尔至强处理器和众核英特尔至强融核协处理器上,只要确保应用充分利用了 CPU 并行化的两个层级(多核并行化和矢量数据并行化),就可以实现最佳性能。

对应用进行矢量化处理的技巧有多种,包括:

使用已经完成矢量化的库,比如英特尔® 数学核心函数库(英特尔® MKL)。 这种方法的优点是,你可以使用优化后的函数库的功能,从而省去大量进行代码矢量化所需的编程工作。

让编译器自动对代码进行矢量化处理。 使用英特尔编译器时,许多开发人员都会依靠这种传统方法 — 而且英特尔编译器表现得非常出色!

明确添加编译指示或指令,比如 OpenMP* SIMD 编译指示/指令。 开发人员越来越多地选择采用这种方法,因为这样实现的矢量化控制水平要高于仅依赖自动矢量化 — 不会受到编程水平过低的限制。

使用矢量内联函数、C++ 矢量类或汇编程序指令插入矢量感知型代码。 这种技巧要求您熟悉如何操作支持矢量化的函数和指令。 以这种方式编写的代码,其便携性大大低于采用上述方法编写的代码。

无论选择采用哪种方法生成矢量化代码,最终的代码必须能够高效执行处理器的矢量单元。 在 DL_MESO 库中,达斯伯里实验室的编程人员使用 OpenMP 4.x 编程标准来提高矢量化性能。

Vectorization Advisor

Vectorization Advisor 是英特尔® Advisor 2016的两个主要特性之一。 英特尔 Advisor 包括 Vectorization AdvisorThreading Advisor

Vectorization Advisor 是一种分析工具,支持您:

  • 对于未矢量化循环,发现阻止代码矢量化的问题,并提供有关如何进行矢量化的提示。
  • 对于使用现代 SIMD 指令的矢量化循环,测量其能效,并提供有关如何提高能效的提示。
  • 对于矢量化循环和未矢量化循环,了解内存布局和数据结构如何能够为矢量提供更多便利。

Vectorization Advisor 可用于任何编译器,但与英特尔编译器同时使用时可以发挥最大作用。 英特尔 Advisor 不仅能够以用户友好型视图的形式显示英特尔编译器生成的各种报告,还能够以精美雅致的格式整合编译时分析结果、贡献二进制静态分析,以及 CPU 热点和精确的循环运行次数等运行时工作负载指标。

合并静态和动态分析的同时,它还会提供一些建议,供您在优化过程中使用。 Vectorization Advisor 可以弥补静态编译器时和动态运行时之间的认知空白,从而提供交互式反馈的优势和丰富的动态二进制分析文件3

英特尔 Advisor Survey:一站式 DL_MESO 性能概述

英特尔 Advisor 用户界面经过精心设计,可以将代码的所有突出矢量化特性整合至一处,形成类似一站式的服务 — 图 2 显示了使用英特尔 Advisor 的矢量化调查分析和运行次数等特性对 Lattice Boltzman 组件进行的初始分析。

英特尔 Advisor 调查报告显示,十大热点耗费了总体执行时间的一半,它们的耗时大体相同,并没有特别耗时的热点;所有耗时的循环占累计程序时间不到 12%。 这类分析具有相对平缓的特征。 平坦的分布曲线对软件开发人员来说通常是一个坏消息,因为为了实现显著的累计工作负载加速,需要查看多个热点,并对各热点进行单独分析和优化,如果没有软件工具的帮助,这项工作将十分费时。

Figure 2. Survey Report with Trip Counts.

图 2. 运行次数调查报告

Vectorization Advisor 支持对热点进行快速分类,如下所示:

  1. 要求一些最小的程序变化(大部分借助 OpenMP 4.x 完成)以支持编译器驱动型 SIMD 并行化的可矢量化、但未矢量化的循环。调查报告中的前四大热点均属于这一类型。
  2. 性能可通过简单优化技巧得以提高的矢量化循环。
  3. 性能受制于数据布局(因此要求进行代码重构以进一步加快执行速度)的矢量化循环。 稍后我们将看到,采用与上述两类循环相应的技巧后,热点 #1 和热点 #2 将变成这一类型的循环。
  4. 性能表现良好的矢量化循环。
  5. 其他(包括不可矢量化内核)。

Vectorization Advisor 不仅提供有关循环的信息,您还可以使用 RecommendationsCompiler Diagnostic Details选项卡了解更多关于具体问题的信息,以及如何解决这些问题。

在我们的案例中,第三个热点 fGetSpeedSite 无法矢量化,因为编译器无法计算循环迭代的次数。 图 3 所示为英特尔 Advisor 针对该问题显示的 Compiler Diagnostic Details窗口,其中还列举了示例以及解决该问题的建议。 按照给出的建议后,该循环轻松完成了矢量化,并从第 2 类循环转变成第 4 类循环。

Figure 3. Interactive Compiler Diagnostics Details window in the Intel Advisor Survey Report.

图 3. 英特尔 Advisor 调查报告中的交互式编译器诊断详情。

即使代码可以矢量化,简单启用矢量化并不总能实现性能提升 — 即成为第 2 类和第 3 类循环。 这就是为什么必须对已经矢量化的循环进行检查,以确认其性能良好的原因。 下一节我们将简要介绍达斯伯里实验室采用英特尔Advisor 处理矢量化效率低下的循环后所实现的优化结果。

触手可及的优化:循环填充

图 4 所示为 DL MESO 配置文件中最热循环的代码。

数组 lbv 将晶格的速度保存在每个维度中,循环数变量 lbsy.nq 因此表示速度的数量。 我们案例中的模型指包含 19 个速度的三维晶格(D3Q19 模式),因此 lbsy.nq 的数值为 19。 最终形成的均衡保存在数组 feq[i] 中。

在初始分析过程中,报告称该循环是标量循环 — 即代码未经矢量化。 只需在 for 循环前面添加 #pragma omp simd,循环就实现了矢量化,且总体运行时间从 13% 降至 9%。 即使进行了这样的添加,也还有大量的优化空间。

int fGetEquilibriumF(double *feq, double *v, double rho)
{
  double modv = v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
  double uv;

  for(int i=0; i<lbsy.nq; i++)
  {
    uv = lbv[i*3] * v[0]
       + lbv[i*3+1] * v[1]
       + lbv[i*3+2] * v[2];

    feq[i] = rho * lbw[i]
           * (1 + 3.0 * uv + 4.5 * uv * uv - 1.5 * modv);
   }
  return 0;
}

图 4. 代码列表 — 用于计算均衡分布的循环

英特尔 Advisor 显示的最新结果表明,编译器生成了两个循环:

  • 一个矢量化循环体,矢量长度 (VL) 为 4 — 即 256 位宽 AVX 寄存器中持有 4 个双精度型
  • 一个标量余数,耗费几乎 30% 的循环时间

此类标量余数完全是不必要的开销。 这种循环余数的存在将对并行效率 — 即可以实现的最大速度提升,产生不利影响。 之所以产生如此大的剩余开销,实际上是因为循环(运行)次数不是矢量长度的整数倍。 编译器对循环进行矢量化处理时,会产生矢量化体,在我们的案例中,该矢量化体执行第 0-15 次循环迭代。 剩下的 3 次迭代 (16-18) 由标量余数代码执行。 由于总循环数非常小,因此剩下的这 3 次迭代将消耗大部分循环时间。 在实现了最佳优化的循环中,尤其是运行次数少的循环中,不应存在剩余代码。

针对这种代码我们可以使用的技巧是,增加循环迭代次数,使其成为 VL 的整数倍,即在本案例中增加为 20。 这种技巧称为 “数据填充”,也是英特尔 Advisor 在针对该循环的 Recommendations窗口中所提出的建议(如图 5)。 为了填充数据,我们需要增加数组 feq[]、lbv[] 和 lbw[] 的大小,以便访问第 20 个(未用)位置时不会造成分段违例或类似问题。

图 11 的第 2 排举例说明了所需要进行的更改。 数值 lbsy.nqpad 是原始循环运行次数与填充值 (NQPAD_COUNT) 之和。

您还可以看到,DL_MESO 开发人员添加了 #pragma 循环次数指令。 将循环数告知编译器后,编译器将看到这个数是矢量长度的整数倍,并面向特定运行次数值优化代码生成,从而在运行时中省略标量余数调用代码。

Figure 5 The Vectorization Advisor recommendations for padding the data.

图 5. Vectorization Advisor 关于填充数据的建议。

DL_MESO 代码中许多类似的均衡分布代码结构都可通过相同方式修改。 在我们的示例中,我们修改了同一个源文件中的其他 3 个循环,每个循环都实现了 15% 的速度提升。

平衡开销和优化折衷

我们将填充技巧应用于前两个循环,但在性能和代码维护两方面付出了一定的代价。

  • 性能方面,填充可避免标量部分产生开销,但我们需要在矢量部分进行额外的计算。
  • 代码维护方面,我们需要重新安排数据结构配置,并可能需要引入依赖于工作负载的编译指示定义。

幸运的是,本案例所实现的性能收益大于性能损失,而且在代码维护方面的负担也比较轻省。

数据布局数组结构转换助力进一步提升性能

矢量化、循环填充和数据对齐技巧将 1 号热点的性能提升了 25-30%。而且,根据英特尔 Advisor4的报告,并行矢量化效率提升了高达 56%。

由于 56% 的效率提升离理想中的 100% 还差得很远,因此达斯伯里实验室的开发人员希望进一步调查阻止循环提高效率的性能障碍。 他们重新查看了 Vector Issues/Recommendations。 这次,Vector Issues栏突出显示了一个新的问题: Possible inefficient memory access patterns present(可能出现低效率内存访问模式)。 给出的相关建议是运行内存访问模式 (MAP) 分析。 Instruction Set Architecture/Traits栏也提出了类似的建议(图 6)。

 Vector Issue, Trait and associated Recommendations.

图 6. “Inefficient Memory Access patterns present”: 矢量问题,特征及相关建议。

MAP 是一种深入的英特尔 Advisor 分析功能,可以识别并详细描述低效率内存访问模式的特征。 为了运行 MAP 工具,DL_MESO 优化人员采用以下基于 GUI 的模式:

  • 首先,开发人员在调查报告的第 2 栏选择相应的复选框,标记第 730 行感兴趣的循环(图 7)。
  • 之后,他们使用 Workflow面板运行 Memory Access Pattern 集合。

Figure 7. Selecting loops for deeper MAP or Dependencies analysis.

图 7. 选择循环以深入分析 MAP 或相关性。

作为 MAP 分析结果测量的高水平 Strides Distribution表明,单位步长和非单位固定步长访问均在该循环中进行(见图 9)。 进一步查看 MAP Problems视图和 Source视图,有利于识别是否出现了与借助 lbv 数组操作对应的步长-3(如果为原始标量版本)或步长-12(如果为填充后的矢量化循环)访问。

固定步长的出现意味着,从迭代到迭代,访问部分数组元素将以可预测、但非线性的方式移动。 在本案例中,步长-3 访问整数元素的 lbv 速度数组意味着在下次迭代中,访问 lbv 数组将通过 3 个整数元素移动。 3 这个值并不令人惊讶,因为相应的表达类似于 lbv[i*3+X]。

Figure 8. Inefficient memory access… Vector Issue and corresponding Recommendations.

图 8. 低效率内存访问......矢量问题和相应的建议。

非连续性固定步长对矢量化非常不利,因为它通常意味着,在矢量化代码版本中,将无法使用单个打包的内存移动指令将所有数组元素加载至最终的矢量寄存器。5另一方面,通过采用结构数组 (AoS) - 数组结构 (SoA) 转换技巧,固定步长访问通常可以转换成单位(连续)步长访问。6值得注意的是,运行 MAP 分析后,最初为 fGetEquilibirumF 中的循环提出的建议已自动更新为采用既定 AoS->SoA 转换的建议(图 8)。

Figure 9. Strides Distribution loop analysis and corresponding tooltip with stride taxonomy explanation.

图 9. Strides Distribution循环分析和相应的工具提示以及步长分类法说明。

达斯伯里实验室的工程师决定就该 lbv 数组采用既定的数据布局优化方法。 它实际上是用于 fGetEquilibrium 中的循环的最新优化技巧。 为了执行这种转换,他们需要将单个 lbv 数组(包括 X、Y 和 Z 维度中的速度)替换为 3 个单独的数组:lbvx、lbvy 和 lbvz。

下图 10 和图 11 汇总了所有与填充和 AoS->SoA 相关的 DL_MESO 循环和数据结构转换,并随附了英特尔 Advisor Vectorization EfficiencyMemory Access Patterns Report指标。

DL_MESO 工程师告诉我们,(相比于填充),尽管重构相对来说比较耗时,且无法轻易实现,但最终实现的速度提升充分说明,这种方法绝对值得一试:fGetEquilibrium 中的循环在优化型版本的基础上将速度再次提升了 2 倍。 许多其他借助 lbv 数组操作的循环也实现了类似的速度提升。

图 10. 填充和数据布局 (AoS -> SoA) 转换对 fGetEquilibriumF 中的循环产生的影响,以及英特尔Advisor Survey 分析、Trip Counts 分析和 MPA 分析的数据。


图 11. 对 fGetEquilibriumF 中的循环进行填充和数据布局 (AoS -> SoA) 转换时的相关数据分配、循环实施和英特尔 Advisor MAP 步长数据。

总结

通过使用 Vectorization Advisor 分析 DL_MESO,并为代码填充部分编译指示,Hartree Centre 成功地将前三大热点耗费的时间缩短了 10%-19%。所有优化均以 Vectorization Advisor 提出的建议为基础。 这项优化工作包括启用矢量化和采用填充优化技巧提高循环性能。 接下来,他们采用类似的技巧处理其他不太重要的热点,使应用的总体速度提升了 18%。

通过将部分变量的数据布局从结构数组转换成数组结构(同样根据 Vectorization Advisor 提出的建议),获得了进一步的性能提升。

尽管实施此次优化工作之际,Vectorization Advisor 仅适用于常规英特尔至强处理器,但如果将相同的优化方法应用于在英特尔至强融核协处理器上运行的代码,也会实现类似的加速效果 — 这显然是一个双赢的局面。

图 2 所示为常规服务器(标记为 “AVX”)和英特尔® 至强融核™ 协处理器(代号 “Knights Corner”)上某个主要关键函数所实现的加速效果。 通过这些优化方法,英特尔® 至强™ 处理器和协处理器分别实现了 2.5 倍和 4.1 倍的速度提升。

Figure 12. The impact of various optimizations (bigger is better).

图 12. 不同优化方法的影响(越大越好)。

总而言之,工程师非常高兴 Vectorization Advisor 能够帮助他们的 DL_MESO 代码实现真正的加速 — 其中一位主要开发人员表示:“这款工具令人叹服,将来在开展至强融核方面的工作时,一定会为我们提供巨大的帮助!”


1 https://software.intel.com/zh-cn/articles/intel-parallel-computing-center-at-hartree-centre-stfc
2 DL_MESO 由两个实施 Lattice Boltzmann Equation (LBE) 和 Dissipative Particle Dynamics (DPD) 方法的模拟程序包组成。 LBE 程序包支持模拟由多个液体组件、溶液和耦合热传递所组成的晶格-气体系统。
3 Vectorization Advisor 要求英特尔编译器收集一套完整的分析数据。 然而,指标的固定子集也适用于借助其他编译器构建的二进制。
4英特尔 Advisor Vectorization Efficiency指标目前仅适用于分析英特尔编译器 16.x (2016) 发布版所编译的代码。
5如欲了解更多有关步长的详细信息,请观看教学视频:https://software.intel.com/zh-cn/videos/memory-access-101https://software.intel.com/zh-cn/videos/stride-and-memory-access-patterns
6如欲了解更多有关数组结构的详细信息,请访问 https://software.intel.com/zh-cn/articles/a-case-study-comparing-aos-arrays-of-structures-and-soa-structures-of-arrays-data-layouts

Fletcher 校验和的快速计算能力

$
0
0

摘要

校验和广泛用于校验应用(如存储和网络)中的数据完整性。 我们提出在英特尔® 处理器上快速计算校验和的方法。 我们没有采用传统的线性方法来计算输入的校验和,而是采用了一种新方法,将数据划分为几个交叉的并行流,并行计算这些分区上的校验和,然后对这些流进行复合计算,使用各分区的校验和计算有效的校验和。

简介

Fletcher 校验和旨在提供一种错误检测功能,近似于 CRC,但是性能更高(在通用处理器上)。

它可执行许多加和计算,其中 C(0) 是对输入字加和,C(1) 是对 C(0) 加和进行加和计算等。一般情况下,和是对模 M 的运算。在某些情况下,M=2K,直接丢掉了高阶。 还有一些情况,M=2K-1。

ZFS是一款常见的文件系统,由 Sun Microsystems 首创,采用开源形式。 该系统的一个特性是保护数据不被损坏。 为此,它广泛使用了基于 Fletcher 的校验和或 SHA-256 散列。 校验和的成本远低于加密散列函数(如 SHA-256),但是仍可从优化中获益。

ZFS 使用的 Fletcher 变量是在四个和(即 C(0)…C(3))的基础上处理 32 位输入数据块 (DWORDS) 并对模 264进行加和得出。

虽然 Fletcher 的标量实施能够获得不错的性能,但是本文展示的方法能够利用英特尔处理器的矢量处理性能进一步提升性能。 该实施是专门针对 ZFS 中使用的 Fletcher 变量定制的,但是这种方法也适用于其他变量。

实施

如果将输入流当作一个 DWORDs(数据)阵列,而且校验和包含 4 个 QWORDS (A、B、C、D;初始化为 0),那么校验和可定义为:

for (i=0; i<end; i++) {
	A += data[i];
	B += A;
	C += B;
	D += C;
}

当尝试对其加速时,显然可以使用 SIMD 指令和注册器。 但是此外还一些其他的方法可以实现。

一种方法是将输入缓冲区分为 4 个连续块(即第一个四等分块,第二个四等分块等),分别计算每个块的校验和,然后将其整合起来。 这种方法存在两个问题。 除非缓冲区的大小固定,否则如果每个缓冲区的尺寸不同,那么各部分的组合方式就会不同。 此外,第一次相加需要一次集合运算,以收集 4 个输入值,而这会增加额外的开销。 这一开销可通过展开循环,从每个分块中读取多个 DWORDS,然后“转置”寄存器来实现。 但是,读取数据仍然会消耗大量的开销。

最有效的方法是采用简单的标量循环,将其实施到 SIMD 指令中,如

.sloop
	vpmovzxdq  data, [buf]	; loads DWORD data into QWORD lanes
	add	   buf, 16
	vpaddq	   a, a, data
	vpaddq	   b, b, a
	vpaddq	   c, c, b
	vpaddq	   d, d, c
	cmp	   buf, end
	jb	   .sloop

这可有效对缓冲区进行条带化处理,以便通道 j能够在 DWORDS (4 i + j) 上计算校验和。 这种方法不会带来额外的开销(如尝试封送输入数据);但是,您需要通过这四个部分的校验和来计算实际的校验和。 所幸,在这种方法中,无需根据输入缓冲区的大小来计算。

如果 “a” 是 “a” ymm 寄存器等的值的 DWORD 指针,那么最后的校验和运算可简化为如下方式:

    A =    a[0] +    a[1] +    a[2] +    a[3];

    B =         -    a[1] -  2*a[2] -  3*a[3]
      +  4*b[0] +  4*b[1] +  4*b[2] +  4*b[3];

    C =                        a[2] +  3*a[3]
      -  6*b[0] - 10*b[1] - 14*b[2] - 18*b[3]
      + 16*c[0] + 16*c[1] + 16*c[2] + 16*c[3];

    D =                             -    a[3]
      +  4*b[0] + 10*b[1] + 20*b[2] + 34*b[3]
      - 48*c[0] - 64*c[1] - 80*c[2] - 96*c[3]
      + 64*d[0] + 64*d[1] + 64*d[2] + 64*d[3];

论证

让 Fi充当处理 i DWORDS 后的校验和。

用一系列的 DWORDS 代表输入流:x1、x2、x3...

展开循环后,您可以看到 F 的元素就是输入 DWORDS 的加权和:

Figure 1

这非常不便,因为 X1(举例)的系数会因考虑的元素个数而异。 对输入进行重新编号会更方便,这样 y1是最新处理的 DWORD,y2是第二新,以此类推。也就是,如果我们处理了 n个 DWORDS,那么 yi = xn+1-i

然后我们发现

Figure 2

或者,一般而言

Figure 3

这就是我们想要的运算结果。 但是,当我们使用 SIMD 指令时,将会每 4 个 DWORD 计算一次校验和。 如果我们考虑 3(对应 y1、y5等),那么,我们实际的运算结果是:

Figure 4

而我们想要该部分和得出的是(用 (4i-3) 替换 i):

Figure 5

如果我们计算以下部分校验和的加权和:

Figure 6

结果显示,(A”3 B”3 C”3 D”3) 与 (A3 B3 C3 D3) 相同,对应的是上图中复合计算的粗体(通道 3)项。

您可以为其他三个通道执行相同的计算。

出色性能

本部分提供的结果是在英特尔® 酷睿™ i7-4770 处理器上测量得出的。 这些测试运行于单个内核,其中英特尔® 睿频加速技术关闭,英特尔® 超线程技术(英特尔® HT 技术)关闭并开启1。 这些测试运行于 16MB 缓冲区上,该缓冲区在高速缓存中进行了预热,这样复合计算的成本便会非常少。

本文中介绍的 SIMD 版本是与知名的标量实施相比,后者是基本的按系数 4 展开循环的标量实施。 结果(周期/DWORD)是2

 HT 关闭HT 开启
标量1.621.56
SIMD0.970.78

得益于处理器的超级标量特性,简单标量版本的性能很高。 由于每个 DWORD 的处理需要 4 次相加,所以每个周期的相加频率为 2.5 (不包括地址更新或比较)。

英特尔® 高级矢量扩展指令集(英特尔® AVX2)SIMD 版本以每周期约 4 次的相加频率运行。

SIMD 在英特尔 HT 技术下展现出更出色的标量计算性能,性能约可提升 24%,而标量版本仅提升 4%。

最后的合并操作需要增加几个周期,所以对于较小的缓冲区而言,标量方法更快;而对于较大的缓冲区,SIMD 方法则可更好地提升性能。 对于支持英特尔® 高级矢量扩展指令集 512(英特尔® AVX-512)的处理器,矢量宽度需要提升 1 倍,以处理 8 个并行操作。 对于较大数据缓冲区,相比英特尔 AVX2,这能够提升近 1 倍的性能。

结论

本文展示了提升校验和性能的方法。 通过利用处理器中的 SIMD 等架构特性和结合使用创新软件技术,能够显著提升性能。

作者

Jim Guilford 和 Vinodh Gopal 是英特尔数据中心事业部的架构师,主要关注领域为与加密和压缩相关的软硬件特性。

注释

1 英特尔技术可能要求激活支持的硬件、特定软件或服务。 实际性能会因您使用的具体系统配置的不同而有所差异。 请咨询您的系统制造商或零售商。

2性能测试中使用的软件和工作负载可能仅在英特尔® 微处理器上针对性能进行了优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能。 上述任何要素的变动都有可能导致测试结果的变化。 请参考其它信息及性能测试(包括结合其它产品使用时的运行性能)以对目标产品进行全面评估。


整理您的数据和代码: 优化和内存 — 第 1 部分

$
0
0

该系列的两篇文章讨论了数据和内存布局如何对性能产生影响,并提出了改进软件性能的具体步骤建议。 这两篇文章中介绍的基本步骤可带来显著的性能提升。 许多关于软件性能优化的文章都解决了某一方面的并行性:分布式内存并行性(如 MPI)、共享内存并行性(如线程)或 SIMD(又称矢量化),但是真正的并行性应对三个方面都进行解决。 虽然这些项目非常重要,但是内存同样重要,而且经常被大家忽略。 软件架构和并行设计的变化会影响内存和性能。

这两篇文章的难度在中等级别。 我们假定读者想要使用常见的 C、C++ 和 Fortran* 编程选项来优化软件性能。 汇编和指令是高级用户使用的编程方式。 作者建议想要使用高级资料的用户去了解一下处理器指令集架构,并阅读一些研究类期刊上的精彩文章来学习一下如何分析和设计数据分区和布局。

整理数据和代码基于两个基本原则:尽可能减少数据移动以及将数据放在要使用的位置附近。 当数据进入或靠近处理器寄存器时,在其从处理器执行单元删除或再次移动前,应尽可能多地使用。

数据分区

我们来考虑一下数据可能会寄存的几个级别。 距离执行单元最近的点是处理器寄存器。 寄存器中的数据可能会立即激活;按照比较或布尔运算进行增量、乘、加运算。 一般而言,多核处理器中的每个核心都有一个私有一级高速缓存(称 L1)。 数据能够非常快地从一级高速缓存移动至寄存器。 可能有许多级高速缓存,通常,最小的末级高速缓存(称 LLC)在处理器的所有内核间共享。 中间级别的高速缓存会因处理器是否共享或私有而呈现差异。 在英特尔平台上,高速缓存负责维护一个平台上的一致性(即使有多个插槽)。 数据从高速缓存移动到寄存器的速度要比数据从主内存获取过来的速度快。

图 1 形象地展示了数据分区、处理器寄存器临近性和相关的访问速度。 数据块距离寄存器越近,移动速度越快,即数据进入寄存器进而执行的延迟越低。 高速缓存的速度最快,即其延迟最低。 主内存次之。 总体上有多个级别的内存,各个级别的内存在第二部分中进行了介绍。 当将页面置换为硬盘或固态盘时,虚拟内存可能会显著减少。 传统的经由互联架构(以太网、infiniband 或其他)进行发送/接收的方式比从本地获取数据的延迟要高。 远程系统上的数据移动通过类似于 MPI 变量的方式访问,其访问速度取决于互联架构 — 以太网、infiniband、英特尔® True Scale 或英特尔® Omni Scale。


图 1. 延迟内存访问,展示相关数据访问速度。

距离执行单元最近的点是处理器寄存器。 考虑到寄存器的数量、将数据加载至寄存器的延迟时间以及内存操作队列的宽度,寄存器中的每个值不可能仅使用一次,加载数据的速度也不可能快到让所有的执行单元全部都运行起来。 当数据靠近执行单元时,应该尽可能再次使用它,直到它从高速缓存或寄存器中移除为止。 事实上,一些变量仅作为变量寄存器存在,从来不会存储到主内存上。 编译器能够很好地识别一个变量作用域何时最适合作为纯寄存器变量,所以不建议在 C/C++ 中使用“寄存器”这一关键词。 编译器已经能够很好地识别该优化,因此可以允许编译器忽略“寄存器”这一关键词。

简要来讲,软件开发人员应该观察代码,思考一下如何使用数据,以及它需要存在多长时间。 问问自己: 我是否应该创建临时变量? 我是否应该创建临时阵列? 我是否需要保留这么多的临时变量? 当实施性能改进流程时,开发人员应该先收集软件性能矩阵,将精力放在模块的数据本地性或使用了绝大部分时间执行代码的代码分支上。 常见的几款性能数据收集实用程序有英特尔® VTune™ Amplifier XE、gprof 和 Tau*。

数据使用和再用

教大家如何使用这两个步骤的一个绝佳示例是矩阵乘法。 针对 3 个 n x n 平方矩阵的矩阵乘法 A = A + B*C 可表示为 3 个简单的嵌套 for循环,如下所示:

for (i=0;i<n; i++)                      // line 136
   for (j = 0; j<n; j++)                // line 137
     for (k=0;k<n; k++)                 // line 138
         A[i][j] += B[i][k]* C[k][j] ;  // line 139

这种定序方式的主要问题是其包含归约运算(138行和 139行)。 139行左侧是一个单值。 虽然编译器在 138行处部分展开循环,以填写 SIMD 寄存器的宽度,并通过元素 B 和 C 产生 4 个或 8 个乘积,这些乘积必须相加到一个单值中。 将 4 个或 8 个乘积加到一个位置是一次约减运算,无法提供相同的并行性能或有效利用 SIMD 设备的全部宽度。 尽可能减少使用或不使用约减运算可以提升并行性能。 看到循环左侧出现单值则表示可能会出现约减。 137一次迭代的数据访问模式如下图 2 所示(i,j=2)。


图 2. 定序;展示矩阵 A 中的单值。

有时,通过重新定序操作可以取消约减。 比如对换两个内部循环的定序。 浮点运算的数量亦是如此。 但是,由于约减运算或将值加到左侧的操作取消,处理器每次都能够利用 SIMD 执行单元的全部宽度和寄存器。 这可以显著提升性能。

for (i=0;i<n; i++)                    // line 136
   for (k = 0; k<n; k++)              // line 137new
     for (j=0;j<n; j++)               // line 138new
       a[i][j] += b[i][k]* c[k][j] ;  // line 139

该操作完成后,元素 A 和 C 将可连续访问。


图 3. 更新的定序,展示连续访问。

最初的 ijk定序采用的是点积方法。 两个矢量的点积用来计算每个 A 元素的值。ikj定序使用 saxpy 或 daxpy 运算。 一个矢量的倍数添加到另一个矢量上。 点积和 axpy 运算均为 1 级 BLAS 程序。 ikj定序中无需约减运算。 C 行的子集乘以矩阵 B 的标量,再加到 A 行的子集上(编译器将根据目标 SIMD 寄存器(SSE4、AVX 或 AVX512)的宽度来决定子集大小)。 循环 137new一次迭代的内存访问上图 3 所示(同样,i,j=2)。

取消点积视图中的约减运算可显著提升性能。 借助 O2 级优化,英特尔编译器和 gcc* 都可以利用 SIMD 寄存器和执行单元生成矢量代码。 此外,英特尔编译器还可自动置换 jk循环的顺序。 这可以在编译器优化报告中看到,该报告可使用 opt-report 编译器选件(Linux* 中的 -qopt-report)获取。 优化报告(默认)存入名为 filename.optrpt的文件。 对于本案例,优化报告包含以下文本片段:

LOOP BEGIN at mm.c(136,4)
   remark #25444: Loopnest Interchanged: ( 1 2 3 ) --> ( 1 3 2 )

该报告还显示,互换循环进行了向量化运算:

LOOP BEGIN at mm.c(137,7)
    remark #15301: PERMUTED LOOP WAS VECTORIZED
 LOOP END

gcc 编译器(版本 4.1.2-55)无法自动执行该循环置换, 需要开发人员手动执行。

此外,将循环分为几块可改进数据再利用,从而帮助提升性能。 在上述展示(图 3)中,对于中间循环每次迭代,引用了两个长度为 n的矢量(以及标量),这两个矢量的每个元素仅使用一次! 对于较大的 n,将每个矢量元素从中间循环迭代之间的高速缓存中删除是非常常见的。 当将循环划分为多块以提供数据再利用时,性能会再次提升。

最后的代码展示了对换的 j 和 k 循环以及添加的块。 代码每次都是在子矩阵或矩阵块上运行,大小为 blockSize,在这一简单案例中,blockSizen倍的代码。

for (i = 0; i < n; i+=blockSize)
   for (k=0; k<n ; k+= blockSize)
      for (j = 0 ; j < n; j+=blockSize)
         for (iInner = i; iInner<j+blockSize; iInner++)
            for (kInner = k ; kInner<k+blockSize; kInner++)
               for (jInner = j ; jInner<j+blockSize ; jInner++)
                 a[iInner,jInner] += b[iInner,kInner] *
                    c[kInner, jInner]

在本示例中,循环 j 的一次迭代的数据访问如下:


图 4 块模型展示。

如果块尺寸选择合适,那么可以假定在三个内部循环运行期间,每个块仍在高速缓存内,甚至在 SIMD 寄存器内。 每个值或 A、B、C 元素在从 SIMD 寄存器或高速缓存中删除前都会使用 blockSize次。 这可将数据复用率提升 blockSize倍。 当使用中等大小的矩阵时,将很少或不会由于划分块而得到性能提升。 随着矩阵尺寸增加,性能差异也会更加明显。

下表展示了在一个系统上使用不同的编译器测得的性能比率。 请注意,英特尔编译器可在 137行和 138行之间自动交换循环。 因此,英特尔编译器在 ijkikj定序上不会展现出明显的差异。 这还使得英特尔编译器的基准性能更高,因此,最后在基准的基础上的加速看上去更低,这是因为英特尔基准性能已经够快了。

订序

矩阵/模块尺寸

Gcc* 4.1.2 -O2
较之基准的加速度/性能比率

英特尔编译器 16.1 -O2
较之基准的加速度/性能比率

ijk

1600

1.0(基准)

12.32

ikj

1600

6.25

12.33

ikj block

1600/8

6.44

8.44

ijk

4000

1.0(基准)

6.39

ikj

4000

6.04

6.38

ikj block

4000/8

8.42

10.53

表 1. 在 gcc* 和英特尔编译器上测量的性能比。

此处的代码示例并不复杂,两款编译器都可以生成 simd 指令。 这是较旧的 gcc 编译器(我们并非在比较编译器而是在提供一个重要的指导原则),它可帮助解释操作顺序和约减运算的影响,即使进入约减运算的数据可以并行完成也不例外。 许多循环更复杂,没有编译器可以识别全部机遇。 因此,建议开发人员考虑一下耗时的代码区域,查看编译器报告以了解编译器是否已将其完成,或考虑亲自将其完成。 第二点要注意的是,当数据变大时,应对其进行分块。 对于包含两个矩阵的小数据,这种方法无法带来性能提升。 对于较大的矩阵,可带来显著的性能提升。 因此,在对全部代码分块前,开发人员应该考虑一下相关的数据尺寸和高速缓存。 通过添加几个嵌套循环和适当的绑定,开发人员可以将性能提升到源代码片段的 2 至 10 倍。 鉴于仅进行了少量的改动,其性能的提升是非常大的。

使用优化的函数库

如要实现真正的高性能,还有一些事情需要做。 使用一个优化函数库(如英特尔® 数学核心函数库(英特尔® MKL))的第 3 级 BLAS例程 DGEMM所带来的性能提升远远高于上述的代码分块所带来的影响。 对于传统的线性代数和傅里叶变换,现代函数库(如英特尔数学核心函数库)带来的优化远胜于本文中介绍的简单分块和定序带来的效果。 如果可以,开发人员应利用这些性能优化的优化函数库。

虽然优化函数库适用于矩阵乘法,但是不适合任何通过分块提升性能的情况。 矩阵乘法为我们提供了一个简单、直观的例子来了解该原则。 这同样适用于有限差分模板。


图 5. 2D 块模型展示。

简单的九点模型将使用上方高亮显示的模块更新块中央的值。 引入 9 个值可以更新一个位置。 当周围的值更新后,其中 6 个值将会再次使用。 如果代码按照显示的方式在域顺序中进行,那么运行状况将会如同临近的图中所示。那么我们可以看到,如果要更新 3 个位置,则需要引入 15 个值。该比率将会逐渐趋近于 1:3。

如果我们将数据再次放到 2D 模块中,那么便会如图 5 中所示,只需要在寄存器中引入 20 个值便可更新 6 个位置;该比率将会逐渐趋近于 1:2。

我鼓励对有限差分技术感兴趣的读者可以阅读一下下面几篇精彩文章:采用各项同性 (ISO) 的三维有限差分 (3DFD) 代码的八项优化措施,作者:Cedric Andreolli。 除了分块之外,我们还通过这两篇文章介绍了一些其他的内存优化技术。

总结

概括来讲,本文中阐述了三个主要步骤,支持开发人员轻松运用到其软件中。 首先,进行定序运算以避免并行约减。 其次,寻找数据重复使用机会,将嵌套循环重新构建到模块中,以便于数据重复使用。 这可将某些操作的性能提升 2 倍。 最后,使用优化函数库(如果可以);相较于传统的开发人员通过简单的重新定序获得的性能,优化函数库的速度明显更快。

完整代码可通过以下链接下载:https://github.com/drmackay/samplematrixcode

第二部分中,我将考虑跨多个内核的并行性,而不仅是 SIMD 寄存器,而且还会介绍一些关于伪共享和结构数组主题的内容。

案例研究: 面向神经细胞模拟优化代码

$
0
0

关于

英特尔举办的英特尔® 现代代码开发人员挑战赛吸引了来自 19 个国家和地区的 130 所高校的 2,000 名学生。参赛者使用英特尔® 至强™ 处理器和英特尔® 至强融核™ 协处理器,对用于 CERN openlab 人脑模拟研究项目的代码进行优化。该研究项目旨在寻找治疗精神分裂症、癫痫症、自闭症等神经疾病的有效方法。参赛者的任务是检查面向细胞群集和 3D 运动的代码,然后通过优化代码修改面向并行性能的算法,从而缩短运行时间,同时保持正确性。

在本文中,Daniel Vea Falguera(其中一位挑战赛获胜者)将与大家分享开发的原始代码和优化后的代码(注:修改后的代码行), 并介绍他所采用的优化方法。 在有些情况下,优化并不奏效,但他就如何修改代码以实现预期目标提供了一些见解。

包括

原始代码

#include <cstring>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <getopt.h>
#include "util.hpp"

优化后的代码

#include <cstring>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <omp.h>
#include <getopt.h>
#include "util.hpp"
#include <malloc.h>     //Useless
#include <mkl.h>        //Useless
#include <cilk/cilk.h> //Useless

修改的代码行:5

优化说明

需要添加 #include <omp.h> 以运行 OpenMP*。 OpenMP 函数和子句均用于优化后的代码。

在开发过程中,已完成的代码中纳入了 malloc.h、mkl.h 和 cilk.h 以优化内存块,但并未实现任何改进。 这种纳入只是为了说明,我们尝试过使用它们。更多关于英特尔® 数学核心函数库(英特尔® MKL)和英特尔® Cilk™ Plus 的信息请参阅“其他优化”部分。

RandomFloatPos

函数 RandomFloatPos() 可用于三次生成随机数字,以在 cellMovementAndDuplication 函数中生成一个随机 3D 位置。

注:本文仅参考了有关计算的函数,因此 “menu printings” 等函数未予以讨论。

原始代码

static float RandomFloatPos() {
    // returns a random number between a given minimum and maximum
    float random = ((float) rand()) / (float) RAND_MAX;
    float a = 0;
    float r = random;
    return a + r;
}

修改的代码行:  3, 4, 5, 6

优化后的代码

static void RandomFloatPos(float input[3],unsigned sedin) {
    // returns a random number between a given minimum and maximum
    __assume_aligned((float*)input, 64);
    unsigned int seed=sedin,i;
    for(i=0;i<3;++i){
        input[i]=(((float)rand_r(&seed))/(float)(RAND_MAX))-0.5;
    }
}

修改的代码行: 1

优化说明

原始函数返回随机数时进行了一些不必要的步骤(例如,为随机数添加零)。由于函数 rand()每次只能执行一条线程,原始代码无法直接并行化。并行化问题使用 rand_r() 函数得到了解决。rand_r() 函数支持同时执行线程。

函数 RandomFloatPos() 被调用来生成一个减去了常量偏移量 0.5 的随机 3D 位置,因此它可简化成包含三次迭代的 for 循环。 优化后的代码也可如此。 偏移量可用于产生从 -0.5 到 0.5 范围内的位置值,从而将点 (0,0,0) 放在细胞运动空间的中间位置。

优化后的函数包含两个参数:input[3] 和 sedin。 Input[3] 返回生成的 3D 随机位置值,而且已对齐内存 (64)。 sedin 作为参数传递,包含每次针对该函数调用时生成的细胞种子的值。

for 循环生成 3D 坐标并将其保存在输入中。

getNorm

原始代码

static float getNorm(float* currArray) {
    // computes L2 norm of input array
    int c;
    float arraySum=0;
    for (c=0; c<3; c++) {
        arraySum += currArray[c]*currArray[c];
    }
    float res = sqrt(arraySum);
    return res;
}

修改的代码行:3, 6, 8, 9

优化后的代码

static float getNorm(float* currArray) {
	// computes L2 norm of input array
	float arraySum=0;
	for (int c=0; c<3; ++c) {
		arraySum += pow(currArray[c],2);
	}
	return sqrt(arraySum);
}

修改的代码行: 5, 7

优化说明

它从输入浮点 currArray 的特定数字阵列开始计算范数。 针对该函数优化代码的方法有两种:

  • 移除 res 变量。
  • 添加 pow() 函数。
    后续开展的广泛研究说明,pow() 并非显著改进。 另外,鉴于英特尔® 至强融核™ 协处理器能够在每个循环执行多次浮点运算,原始 currArray[c]*currArray[c] 在那些案例中的速度更快。 英特尔编译器对 pow() 函数进行了矢量化处理,但因此添加了更多代码,因此降低了运行时速度。

getL2Distance

该函数用于确定 3D 空间中两点之间的线性距离。

原始代码

static float getL2Distance(float pos1x, float pos1y, float pos1z, float
pos2x, float pos2y, float pos2z) {
	// returns distance (L2 norm) between two positions in 3D
	float distArray[3];
	distArray[0] = pos2x-pos1x;
	distArray[1] = pos2y-pos1y;
	distArray[2] = pos2z-pos1z;
	float l2Norm = getNorm(distArray);
	return l2Norm;
}

修改的代码行: 1, 2, 5, 6, 7, 8, 9

优化后的代码

static float getL2Distance(float* pos1, float* pos2) {
	// returns distance (L2 norm) between two positions in 3D
	float distArray[3] __attribute__((aligned(64)));
	distArray[0] = pos2[0]-pos1[0];
	distArray[1] = pos2[1]-pos1[1];
	distArray[2] = pos2[2]-pos1[2];
	return getNorm(distArray);
}

修改的代码行: 1-8

优化说明

原始函数有 6 个代表 2 个 3D 点的输入,以计算这 2 点之间的距离。 优化后的函数只有 2 个输入,分别为 2 个代表 3D 点的 3 要素阵列。 其中使用的变量已对齐。

优化后的函数看起来是 SIMD 可执行文件,但当使用矢量符号(例如 P[0:2]=a[0:2]*b[0:2])时,执行速度降低了。 定义并使用初等函数也许能够进一步优化该函数。

如欲阅读有关初等函数的白皮书,请访问:http://software.intel.com/sites/default/files/article/181418/whitepaperonelementalfunctions.pdf

produceSubstances

该函数将每细胞位置的物质集中度增加至最大限值:每细胞位置 1 单位。

原始代码

static void produceSubstances(float**** Conc, float** posAll, int* typesAll, int L, int n){

	produceSubstances_sw.reset();
		// increases the concentration of substances at the location of the     cells
		float sideLength = 1/(float)L; // length of a side of a diffusion voxel
		int c, i1, i2, i3;
		for (c=0; c< n; c++) {
			i1 = std::min((int)floor(posAll[c][0]/sideLength),(L-1));
			i2 = std::min((int)floor(posAll[c][1]/sideLength),(L-1));
			i3 = std::min((int)floor(posAll[c][2]/sideLength),(L-1));
		if (typesAll[c]==1) {
			Conc[0][i1][i2][i3]+=0.1;
			if (Conc[0][i1][i2][i3]>1) {
				Conc[0][i1][i2][i3]=1;
			}
		} else {
			Conc[1][i1][i2][i3]+=0.1;
			if (Conc[1][i1][i2][i3]>1) {
				Conc[1][i1][i2][i3]=1;
			}
		}
	}
	produceSubstances_sw.mark();
}

修改的代码行: 1, 5, 6, 8, 9, 10, 11, 13, 14, 17, 18, 19

优化后的代码

static void produceSubstances(int L, float Conc[2][L][L][L], float posAll[][3], int* typesAll, int n) {

	produceSubstances_sw.reset();
	// increases the concentration of substances at the location of the  cells

	const int auxL=L;
	--L;
	int c,i[3] __attribute__((aligned(32))); //i array aligned
	omp_set_num_threads(240);

	#pragma omp parallel for schedule(static) private(i,c)
	for (c=0; c< n; ++c) {
		__assume_aligned((int*)i, 32);
		__assume_aligned((float*)posAll, 64);
		i[0] = std::min((int)floor(posAll[c][0]*auxL),L);
		i[1] = std::min((int)floor(posAll[c][1]*auxL),L);
		i[2] = std::min((int)floor(posAll[c][2]*auxL),L);

		if (typesAll[c]==1) {
			(Conc[0][i[0]][i[1]][i[2]]>0.9)?
			Conc[0][i[0]][i[1]][i[2]]=1 :
			Conc[0][i[0]][i[1]][i[2]]+=0.1;
		} else {
			(Conc[1][i[0]][i[1]][i[2]]>0.9)?
			Conc[1][i[0]][i[1]][i[2]]=1 :
			Conc[1][i[0]][i[1]][i[2]]+=0.1;
		}
	}
	produceSubstances_sw.mark();
}

修改的代码行: 1,6-11, 13-17, 20-22, 24-26

优化说明

化后的函数输入修改后的序列,以在函数的标头中定义阵列的大小。 这样我们可以不必使用指示器,而是直接处理阵列,以便编译器事先知道我们向该函数传递的要素的大小。

原始代码使用指示器初始化阵列,但由于这些阵列是静态的(长度固定,无法更改),因此不使用指示器,直接将其声明为已定义大小的阵列会更加简单、快速。

优化后的代码包括使用面向函数的 OpenMP 并行,以及静态调度,以将负载平均分配至其他内核。 这是因为,该函数可以并行执行,对结果不会造成影响,即 for 循环的每次循环都可以在不影响下次迭代的情况下独立执行。

在主代码(稍后介绍)执行过程中,该函数调用了多次,每次都增加了 n 值,因此,尽管可以实现函数并行化,但这仅适用于 n 值较小(小于 10000)的情况。 否则,额外的开销会降低代码的速度。

运算 posAll[c][0]/sideLength 与 (posAll[c][0]/1/L) 或 (posAll[c][0]*L) 相同。 posAll[c][0]/sideLength 的计算次数更少,但可生成相同的结果,因此比较有利。

L 变量的使用在优化时进行了更改。 原始代码 3 次使用运算 L-1,从而使用了 3 次额外运算。 在优化版本中,我们只需简单地递减之前的 L 变量。 auxL 常量(用于优化,因为只读变量比读/写变量快)保存 L 的原值,在该函数执行期间保持不变。

同样,我们改变了使用 Conc 变量的方法,以优化代码。 在原始代码中,if 子句使 Conc 变量递增了 0.1,然后检查该值是否大于 1,如果是,将该值限制在 1,从而使之前的加法成为无用功。 该方法用于检查 Conc 是否大于 0.9,如果是,将 Conc 的值设定在 1,如果不是,使 Conc 递增 0.1。

runDiffusionStep

该函数分为两部分。 第一部分将 Conc 变量拷贝至 tempConc。 第二部分迭代 Conc 阵列校验上限和 3D 边界。

原始代码

static void runDiffusionStep(float**** Conc, int L, float D) {
	runDiffusionStep_sw.reset();
	// computes the changes in substance concentrations due to diffusion

	int i1,i2,i3, subInd;
	float tempConc[2][L][L][L];
	for (i1 = 0; i1 < L; i1++) {
		for (i2 = 0; i2 < L; i2++) {
			for (i3 = 0; i3 < L; i3++) {
				tempConc[0][i1][i2][i3] = Conc[0][i1][i2][i3];
				tempConc[1][i1][i2][i3] = Conc[1][i1][i2][i3];
			}
		}
	}

	int xUp, xDown, yUp, yDown, zUp, zDown;

	for (i1 = 0; i1 < L; i1++) {
		for (i2 = 0; i2 < L; i2++) {
			for (i3 = 0; i3 < L; i3++) {
				xUp = (i1+1);
				xDown = (i1-1);
				yUp = (i2+1);
				yDown = (i2-1);
				zUp = (i3+1);
				zDown = (i3-1);
				for (subInd = 0; subInd < 2; subInd++) {
					if (xUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][xUp][i2][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (xDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][xDown][i2][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (yUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][yUp][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (yDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][yDown][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (zUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][i2][zUp]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (zDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][i2][zDown]-tempConc[subInd][i1][i2][i3])*D/6;
					}
				}
			}
		}
	}
	runDiffusionStep_sw.mark();
}

修改的代码行: 1, 5, 9-12, 16, 21-45

优化后的代码

static void runDiffusionStep(int L, float Conc[2][L][L][L], float D) {
	runDiffusionStep_sw.reset();
	// computes the changes in substance concentrations due to diffusion
	int i1,i2,i3,auxx;
	const float auxD=D/6;
	const int auxL=L-1;
	float tempConc[2][L][L][L] __attribute__((aligned(64)));
	omp_set_num_threads(240);
	#pragma omp parallel
	{
		#pragma omp for schedule(static) private(i1,i2) collapse(2)
		for (i1 = 0; i1 < L; ++i1) {
			for (i2 = 0; i2 < L; ++i2) {
				memcpy(tempConc[0][i1][i2],
				Conc[0][i1][i2],sizeof(float)*L);
				memcpy(tempConc[1][i1][i2],
				Conc[1][i1][i2],sizeof(float)*L);
			}
		}

		#pragma omp for schedule(static) private(i1,i2,i3) collapse(2)
		for (i1 = 0; i1 < L; ++i1) {
			for (i2 = 0; i2 < L; ++i2) {
				Conc[0][i1][i2][0] += (tempConc[0][i1][i2][1]-
				tempConc[0][i1][i2][0])*auxD;
				Conc[1][i1][i2][0] += (tempConc[1][i1][i2][1]-
				tempConc[1][i1][i2][0])*auxD;
				for (i3 = 1; i3 < auxL; ++i3) {
					const float aux=tempConc[0][i1][i2][i3];
					const float aux1=tempConc[1][i1][i2][i3];
					__assume_aligned((float*)tempConc[0], 64);
					__assume_aligned((float*)tempConc[1], 64);
					__assume_aligned((float*)Conc[0], 64);
					__assume_aligned((float*)Conc[1], 64);
					if (i1<auxL) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][(i1+1)][i2][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][(i1+1)][i2][i3]-aux1)*auxD;
					}
					if (i1>0) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][(i1-1)][i2][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][(i1-1)][i2][i3]-aux1)*auxD;
					}
					if (i2<auxL) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][i1][(i2+1)][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][i1][(i2+1)][i3]-aux1)*auxD;
					}
					if (i2>0) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][i1][(i2-1)][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] += (tempConc[1][i1][(i2-1)][i3]-aux1)*auxD;
					}
					Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3+1)]-aux)*auxD;
					Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3+1)]-aux1)*auxD;
					Conc[0][i1][i2][i3] +=  (tempConc[0][i1][i2][(i3-1)]-aux)*auxD;
					Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3-1)]-aux1)*auxD;
				}
			Conc[0][i1][i2][auxL-1] += (tempConc[0][i1][i2][auxL]-tempConc[0][i1][i2][auxL-1])*auxD;
			Conc[1][i1][i2][auxL-1] += (tempConc[1][i1][i2][auxL]-tempConc[0][i1][i2][auxL-1])*auxD;
		}
	}
}

修改的代码行: 1, 4-11, 14-17, 21, 24-64

优化说明

该函数包含两个主要部分:第一部分将 Conc 变量(使用 for 循环)拷贝至 tempConc,第二部分迭代 Conc 阵列,同时不断校验上维边界和下维边界。 这两部分可以轻松实现并行化:所有线程拷贝 Conc 的除法,然后所有线程执行下一个大循环的除法。 该函数中开销最大的部分在第二个大循环中,每次迭代期间,都需要增减坐标并校验边界。 如果计算的坐标在边界中,将执行计算。

优化后的函数可创建所有线程。 它们的 memcpy 循环部分在每条线程中执行,全部完成后,开始执行维度循环。 使用 memcpy 会增加迭代进行的拷贝次数。

变量的优化方法与之前函数相同:尽量使用常量,并对齐阵列。

两个循环在 OpenMP 编译指示标头下方‘重叠’,这样:

  • 第一个循环在
    #pragma omp for schedule(static) private(i1,i2) collapse(2) 下方
  • 第二个循环在
    #pragma omp for schedule(static) private(i1,i2,i3) collapse(2) 下方

第一个循环 — tempConc copy 循环通过多线程化实现优化。 该循环借助‘parallel for’完成多线程化,并通过使用 memcpy(如上所述)移动每次迭代的大部分数据,以实现优化。 我不知道将 Conc 拷贝至 tempConc 的过程中是否占用了所有可用内存带宽,因此使用所有可用内存带宽(约为 L*float(64bit)*240)也是实现改进的一种方法。

第二个循环(处理维度)的优化方式不同。

初始优化必须涉及 i3 边界校验。 这种校验位于第 4 行代码:

Conc[0][i1][i2][0]
Conc[1][i1][i2][0]

Conc[0][i1][i2][auxL-1]
Conc[1][i1][i2][auxL-1]

如果循环是 L-2 迭代(i3 从 1 前往 auxL),可以避免 i3 边界校验,因此我们手动添加第一次和最后一次运算。 受到这四行代码的限制,i3 边界永远不会被超越:

Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3+1)]-aux)*auxD;
Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3+1)]-aux1)*auxD;
Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3-1)]-aux)*auxD;
Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3-1)]-aux1)*auxD;

如果未达到边界,将执行 if 代码中的代码。

也就是说,如果删除其中的 if 代码,该循环将得以改进。我们可以手动展开该循环,仔细观察边界,然后将任务分配至线程和 OpenMP 任务函数。

runDecayStep

该函数迭代所有 Conc 变量,以完成每次迭代上的乘法。

原始代码

static void runDecayStep(float**** Conc, int L, float mu) {
	runDecayStep_sw.reset();
	// computes the changes in substance concentrations due to decay
	int i1,i2,i3;
	for (i1 = 0; i1 < L; i1++) {
		for (i2 = 0; i2 < L; i2++) {
			for (i3 = 0; i3 < L; i3++) {
				Conc[0][i1][i2][i3] = Conc[0][i1][i2][i3]*(1-mu);
				Conc[1][i1][i2][i3] = Conc[1][i1][i2][i3]*(1-mu);
			}
		}
	}
	runDecayStep_sw.mark();
}

修改的代码行: 1, 8, 9

优化后的代码

static void runDecayStep(int L, float Conc[2][L][L][L], float mu) {
	runDecayStep_sw.reset();
	// computes the changes in substance concentrations due to decay
	const float muu=1-mu;
	int i1,i2,i3;
	omp_set_num_threads(240);
	#pragma omp parallel for schedule(static) private(i1,i2,i3) collapse(3)
	#pragma simd
	for (i1 = 0; i1 < L; ++i1) {
		for (i2 = 0; i2 < L; ++i2) {
			for (i3 = 0; i3 < L; ++i3) {
				__assume_aligned((float*)Conc[0][i1][i2], 64);
				__assume_aligned((float*)Conc[1][i1][i2], 64);
				Conc[0][i1][i2][i3] = Conc[0][i1][i2][i3]*muu;
				Conc[1][i1][i2][i3] = Conc[1][i1][i2][i3]*muu;
			}
		}
	}
	runDecayStep_sw.mark();
}

修改的代码行: 1, 4, 6-8, 12-15

优化说明

该函数只需一些简单的调整就可实现优化。

  • 函数的输入阵列在标头上定义。
  • 变量的优化方式与之前的函数一样(尽量使用常量,并对齐阵列)。
  • 循环在 OpenMP 编译指示标头下方‘重叠’。
  • 使用 #pragma simd 循环。 借助 SIMD 指令对该循环进行矢量化处理,可缩短优化后代码的执行时间。 使用 SIMD 指令支持同时执行乘法运算。

其他优化方法包括最大限度发挥英特尔 MKL 函数的作用。 英特尔 MKL 函数面向大数据阵列而优化,因此如果 Conc 阵列扁平化至一个维度,英特尔 MKL 的执行速度会更快。

cellMovementAndDuplication

该函数用于生成每个细胞及每个细胞副本的随机运动。 但并不是所有细胞副本。

原始代码

static int cellMovementAndDuplication(float** posAll, float* pathTraveled,
 int* typesAll, int* numberDivisions, float pathThreshold, int
 divThreshold, int n) {
	cellMovementAndDuplication_sw.reset();
	int c;
	currentNumberCells = n;
	float currentNorm;
	float currentCellMovement[3];
	float duplicatedCellOffset[3];
	for (c=0; c<n; c++) {
		// random cell movement
		currentCellMovement[0]=RandomFloatPos()-0.5;
		currentCellMovement[1]=RandomFloatPos()-0.5;
		currentCellMovement[2]=RandomFloatPos()-0.5;
		currentNorm = getNorm(currentCellMovement);
		posAll[c][0]+=0.1*currentCellMovement[0]/currentNorm;
		posAll[c][1]+=0.1*currentCellMovement[1]/currentNorm;
		posAll[c][2]+=0.1*currentCellMovement[2]/currentNorm;
		pathTraveled[c]+=0.1;
		// cell duplication if conditions fulfilled
		if (numberDivisions[c]<divThreshold) {
			if (pathTraveled[c]>pathThreshold) {
				pathTraveled[c]-=pathThreshold;
				numberDivisions[c]+=1; // update number of divisions this cell has undergone
				currentNumberCells++;  // update number of cells in
				 the simulation
				numberDivisions[currentNumberCells-1]=numberDivisions[c]; // update number of divisions the  duplicated cell has undergone
				typesAll[currentNumberCells-1]=-typesAll[c];
				// assign type of duplicated cell (opposite to current cell)

				// assign location of duplicated cell
				duplicatedCellOffset[0]=RandomFloatPos()-0.5;
				duplicatedCellOffset[1]=RandomFloatPos()-0.5;
				duplicatedCellOffset[2]=RandomFloatPos()-0.5;
				currentNorm = getNorm(duplicatedCellOffset);
				posAll[currentNumberCells-1][0]=posAll[c][0]+0.05*duplicatedCellOffset[0]/currentNorm;
				posAll[currentNumberCells-1][1]=posAll[c][1]+0.05*duplicatedCellOffset[1]/currentNorm;
				posAll[currentNumberCells-1][2]=posAll[c][2]+0.05*duplicatedCellOffset[2]/currentNorm;
			}
		}
	}
	cellMovementAndDuplication_sw.mark();
	return currentNumberCells;
}

修改的代码行: 1, 12-14, 16-18, 21, 22, 24-34, 36-38

优化后的代码

static int cellMovementAndDuplication(float posAll[][3], float* pathTraveled,
 int* typesAll, int* numberDivisions, float pathThreshold, int
 divThreshold, int n) {
	cellMovementAndDuplication_sw.reset();
	int c,currentNumberCells = n;
	float currentNorm;
	unsigned int seed=rand();
	float currentCellMovement[3] __attribute__((aligned(64)));
	float duplicatedCellOffset[3] __attribute__((aligned(64)));
	omp_set_num_threads(240);

	#pragma omp parallel for simd schedule (static) shared(posAll) private(c,currentNorm,currentCellMovement)

	for (c=0; c<n; ++c) {
		// random cell movement
		RandomFloatPos(currentCellMovement,seed+c);
		currentNorm = getNorm(currentCellMovement)*10;
		__assume_aligned((float*)posAll, 64);
		__assume_aligned((float*)currentCellMovement, 64);
		posAll[c][0:3]+=currentCellMovement[0:3]/currentNorm;
		__assume_aligned((float*)pathTraveled, 64);
		pathTraveled[c]+=0.1;
	}
	seed=rand();
	for (c=0; c<n; ++c) {
		if ((numberDivisions[c]<divThreshold) && (pathTraveled[c]>pathThreshold)) {
			pathTraveled[c]-=pathThreshold;
			++numberDivisions[c]; // update number of divisions this cell has undergone
			numberDivisions[currentNumberCells]=numberDivisions[c]; // update number of divisions the duplicated cell has undergone
			typesAll[currentNumberCells]=-typesAll[c]; // assign type of
			 duplicated cell (opposite to current cell)

			// assign location of duplicated cell
			RandomFloatPos(duplicatedCellOffset,seed+c); //The seed+c value will be different for each thread and iteration, this way the random value is random always.
			currentNorm = getNorm(duplicatedCellOffset)*20;
			__assume_aligned((float*)posAll, 64);
			__assume_aligned((float*)duplicatedCellOffset, 64);
			posAll[currentNumberCells][0:3]=posAll[c][0:3]+duplicatedCellOffset[0:3]/currentNorm;
			++currentNumberCells; // update number of cells in the simulation
		}
	}
	cellMovementAndDuplication_sw.mark();
	return currentNumberCells;
}

修改的代码行: 1, 8-12, 16-21, 26, 28, 29, 34, 36-39

优化说明

面向 cellMovementAndDuplication 的优化代码分为两部分,其优化方法与之前的函数相同:

  • RandomFloatPos() 函数。 这一优化后的函数可以实现多线程化,并返回整个位置阵列(3D 位置阵列)。
  • 包含 currentNorm 的代码行可通过 SIMD 实现矢量化,也可以以数学的方式进行简化。

另外,优化后的代码更新了标头,并对齐了阵列。

在调用 seed=rand() 之前,新函数 RandomFloatPos() 要求一个随机生成的种子,而且每条线程需要该种子不同的值。 为此,每条线程增加了随机生成的种子值,以对应循环迭代。 这样,每条线程的数字既是随机的,又各不相同。

为了实现该函数的多线程化,我将其分成两个大循环。 第一个(随机细胞运动)可轻松实现并行化。 第二个循环(从 seed=rand(); 开始)必须在单条线程中执行,因为每次迭代都依赖之前迭代的值。

runDiffusionClusterStep

该函数用于根据物质的渐变决定细胞运动。

原始代码

static void runDiffusionClusterStep(float**** Conc, float** movVec, float** posAll, int* typesAll, int n, int L, float speed) {
	runDiffusionClusterStep_sw.reset();
	// computes movements of all cells based on gradients of the two substances
	float sideLength = 1/(float)L; // length of a side of a diffusion voxel

	float gradSub1[3];
	float gradSub2[3];
	float normGrad1, normGrad2;
	int c, i1, i2, i3, xUp, xDown, yUp, yDown, zUp, zDown;

	for (c = 0; c < n; c++) {
		i1 = std::min((int)floor(posAll[c][0]/sideLength),(L-1));
		i2 = std::min((int)floor(posAll[c][1]/sideLength),(L-1));
		i3 = std::min((int)floor(posAll[c][2]/sideLength),(L-1));

		xUp = std::min((i1+1),L-1);
		xDown = std::max((i1-1),0);
		yUp = std::min((i2+1),L-1);
		yDown = std::max((i2-1),0);
		zUp = std::min((i3+1),L-1);
		zDown = std::max((i3-1),0);

		gradSub1[0] = (Conc[0][xUp][i2][i3]-Conc[0][xDown][i2][i3])/(sideLength*(xUp-xDown));
		gradSub1[1] = (Conc[0][i1][yUp][i3]-Conc[0][i1][yDown][i3])/(sideLength*(yUp-yDown));
		gradSub1[2] = (Conc[0][i1][i2][zUp]-Conc[0][i1][i2][zDown])/(sideLength*(zUp-zDown));
		gradSub2[0] = (Conc[1][xUp][i2][i3]-Conc[1][xDown][i2][i3])/(sideLength*(xUp-xDown));
		gradSub2[1] = (Conc[1][i1][yUp][i3]-Conc[1][i1][yDown][i3])/(sideLength*(yUp-yDown));
		gradSub2[2] = (Conc[1][i1][i2][zUp]-Conc[1][i1][i2][zDown])/(sideLength*(zUp-zDown));
		normGrad1 = getNorm(gradSub1);
		normGrad2 = getNorm(gradSub2);
		if ((normGrad1>0)&&(normGrad2>0)) {
			movVec[c][0]=typesAll[c]*(gradSub1[0]/normGrad1-gradSub2[0]/normGrad2)*speed;
			movVec[c][1]=typesAll[c]*(gradSub1[1]/normGrad1-gradSub2[1]/normGrad2)*speed;
			movVec[c][2]=typesAll[c]*(gradSub1[2]/normGrad1-gradSub2[2]/normGrad2)*speed;
		} else {
			movVec[c][0]=0;
			movVec[c][1]=0;
			movVec[c][2]=0;
		}
	}
	runDiffusionClusterStep_sw.mark();
}

修改的代码行: 1-2, 5-10, 13-39 

优化后的代码

static void runDiffusionClusterStep(int L, float Conc[2][L][L][L], float movVec[][3], float posAll[][3], int* typesAll, int n, float speed) {
	runDiffusionClusterStep_sw.reset();
	// computes movements of all cells based on gradients of the two substances
		const float auxL=L;
			--L;
	float gradSub[6] __attribute__((aligned(64)));
	float aux[3] __attribute__((aligned(64)));
		int i[3] __attribute__((aligned(32)));
	float normGrad[2] __attribute__((aligned(64)));
	int c, xUp, xDown, yUp, yDown, zUp, zDown;
	   omp_set_num_threads(240);

	#pragma omp parallel for schedule(static) private(i,xUp,xDown,yUp,yDown,zUp,zDown,gradSub,normGrad,aux) if (n>240)
	for (c = 0; c < n; ++c) {
		__assume_aligned((int*)i, 32);
		__assume_aligned((float*)posAll, 64);
		__assume_aligned((float*)gradSub, 64);
		__assume_aligned((float*)normGrad, 64);
		__assume_aligned((float*)movVec, 64);
		__assume_aligned((float*)typesAll, 64);
		i[0:3] = std::min((int)floor(posAll[c][0:3]*auxL),L)-1;
		xDown = std::max(i[0],0);
		yDown = std::max(i[1],0);
		zDown = std::max(i[2],0);
		xUp = std::min((i[0]+2),L);
		yUp = std::min((i[1]+2),L);
		zUp = std::min((i[2]+2),L);
		aux[0]=auxL/((xUp-xDown));
		aux[1]=auxL/((yUp-yDown));
		aux[2]=auxL/((zUp-zDown));
		gradSub[0] = (Conc[0][xUp][i[1]][i[2]]-Conc[0][xDown][i[1]][i[2]])*aux[0];
		gradSub[1] = (Conc[0][i[0]][yUp][i[2]]-Conc[0][i[0]][yDown][i[2]])*aux[1];
		gradSub[2] = (Conc[0][i[0]][i[1]][zUp]-Conc[0][i[0]][i[1]][zDown])*aux[2];
		normGrad[0] = getNorm(gradSub);
		if (normGrad[0]>0){
			gradSub[3] = (Conc[1][i[0]][yUp][i[2]]-Conc[1][i[0]][yDown][i[2]])*aux[1];
			gradSub[4] = (Conc[1][xUp][i[1]][i[2]]-Conc[1][xDown][i[1]][i[2]])*aux[0];
			gradSub[5] = (Conc[1][i[0]][i[1]][zUp]-Conc[1][i[0]][i[1]][zDown])*aux[2];
			normGrad[1] = getNorm(gradSub+3);
			if ( normGrad[1]>0) {
				movVec[c][0:3]=typesAll[c]*(gradSub[0:3]/normGrad[0]-gradSub[3:3]/normGrad[1])*speed;
			} else movVec[c][0:3]=0;
		}
	}
	runDiffusionClusterStep_sw.mark();
}

修改的代码行: 1, 5-10, 12-14, 12-43

优化说明

该函数通过一些小小的简化、矢量化和多线程化行为实现了优化,与其他函数大体相同。

  • 输入阵列的维度在标头中规定。
  • 阵列对齐。
  • 尽量将变量替换成常量。
  • OpenMP 并行化。
  • 简化运算。
  • 简化矢量化。

if 条件可以过滤掉一部分运算。 例如,如果 normGrad[0] 等于或小于 0,则不必计算 normGrad[1]。 使用 if 条件可以减少对比和无用运算的次数。

使用更多 SIMD 优化,可进一步优化该函数。 不过,如需使用 SIMD 运算,max 和 min 函数必须以不同的方式实施。

getEnergy

该函数通过假设整卷的分布均匀以及确定一定数量的目标细胞数,计算细胞子集的能量测度。

原始代码

Original Code
static float getEnergy(float** posAll, int* typesAll, int n, float spatialRange, int targetN) {
	getEnergy_sw.reset();
	// Computes an energy measure of clusteredness within a subvolume. The
	// size of the subvolume is computed by assuming roughly uniform
	// distribution within the whole volume, and selecting a volume
	// comprising approximately targetN cells.
	int i1, i2;
	float currDist;
	float** posSubvol=0; // array of all 3 dimensional cell positions
	posSubvol = new float*[n];
	int typesSubvol[n];
	float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2;
	if(quiet < 1)
		printf("subVolMax: %f\n", subVolMax);
	int nrCellsSubVol = 0;
	float intraClusterEnergy = 0.0;
	float extraClusterEnergy = 0.0;
	float nrSmallDist=0.0;

	for (i1 = 0; i1 < n; i1++) {
		posSubvol[i1] = new float[3];
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]-0.5)<subVolMax)) {
			posSubvol[nrCellsSubVol][0] = posAll[i1][0];
			posSubvol[nrCellsSubVol][1] = posAll[i1][1];
			posSubvol[nrCellsSubVol][2] = posAll[i1][2];
			typesSubvol[nrCellsSubVol] = typesAll[i1];
			nrCellsSubVol++;
		}
	}

	for (i1 = 0; i1 < nrCellsSubVol; i1++) {
		for (i2 = i1+1; i2 < nrCellsSubVol; i2++) {
			currDist = getL2Distance(posSubvol[i1][0],posSubvol[i1][1],posSubvol[i1][2],posSubvol[i2][0],
			posSubvol[i2][1],posSubvol[i2][2]);
			if (currDist<spatialRange) {
				nrSmallDist = nrSmallDist+1;//currDist/spatialRange;
				if (typesSubvol[i1]*typesSubvol[i2]>0) {
					intraClusterEnergy = intraClusterEnergy+fmin(100.0,spatialRange/currDist);
				} else {
					extraClusterEnergy = extraClusterEnergy+fmin(100.0,spatialRange/currDist);
				}
			}
		}
	}
	float totalEnergy = (extraClusterEnergy-intraClusterEnergy)/(1.0+100.0*nrSmallDist);
	getEnergy_sw.mark();
	return totalEnergy;
}

修改的代码行: 1, 22, 34, 35, 37-41, 46, 48

优化后的代码

static float getEnergy(float posAll[][3], int* typesAll, int n, float spatialRange, int targetN) {
	getEnergy_sw.reset();
	// Computes an energy measure of clusteredness within a subvolume. The
	// size of the subvolume is computed by assuming roughly uniform
	// distribution within the whole volume, and selecting a volume
	// comprising approximately targetN cells.
	int i1, i2;
	float currDist;
	float posSubvol[n][3] __attribute__((aligned(64)));
	int typesSubvol[n] __attribute__((aligned(64)));
	const float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2;
	if(quiet < 1)printf("subVolMax: %f\n", subVolMax);

	int nrCellsSubVol = 0;
	float intraClusterEnergy = 0.0;
	float extraClusterEnergy = 0.0;
	float nrSmallDist=0.0;
	for (i1 = 0; i1 < n; ++i1) {
		__assume_aligned((float*)posAll, 64);
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]-
			0.5)<subVolMax)) {
			__assume_aligned((float*)posSubvol[nrCellsSubVol], 64);
			__assume_aligned((float*)typesAll, 64);
			__assume_aligned((int*)typesSubvol, 64);
			posSubvol[nrCellsSubVol][0] = posAll[i1][0];
			posSubvol[nrCellsSubVol][1] = posAll[i1][1];
			posSubvol[nrCellsSubVol][2] = posAll[i1][2];
			typesSubvol[nrCellsSubVol] = typesAll[i1];
			++nrCellsSubVol;
		}
	}
	omp_set_num_threads(240);
	#pragma omp parallel for schedule(static) reduction(+:nrSmallDist,intraClusterEnergy,extraClusterEnergy) private(i1,i2,currDist)
	for (i1 = 0; i1 < nrCellsSubVol; ++i1) {
		for (i2 = i1+1; i2 < nrCellsSubVol; ++i2) {
			currDist = getL2Distance(posSubvol[i1],posSubvol[i2]);
			if (currDist<spatialRange) {
				++nrSmallDist; //currDist/spatialRange;
				(typesSubvol[i1]*typesSubvol[i2]>0)? intraClusterEnergy += fmin(100.0,spatialRange/currDist) :
				extraClusterEnergy += fmin(100.0,spatialRange/currDist);
			}
		}
	}
	getEnergy_sw.mark();
	return (extraClusterEnergy-intraClusterEnergy)/(1.0+100.0*nrSmallDist);
}

修改的代码行: 1, 9-11, 19, 22-24, 32, 33, 36-40

优化说明

该代码中的一部分比较容易优化,包括‘new’构造函数、部分是常量的变量,以及第二个循环依赖第一个循环的事实。 第一个循环由于依赖 nrCellsSubVol 的值,因此无法实现并行化,再加上该循环中包括三个条件的 if,其并行化难度更大。 不过,第二个循环可以轻松实现并行化。

该函数的具体优化方式与其他函数相同。

  • 输入阵列的维度在标头中规定。
  • 尽量使用常量。
  • 对齐阵列。
  • 使用减法在第二个 ‘for loop’ 上实现 OpenMP 并行化。

下一个优化步骤是对第一个循环的 if 条件进行并行化处理。很难,但可以实现,并且还可以提升性能。

getCriterion

该函数用于确定子集中的细胞是否在集群中。

注:原始代码和优化后的代码都是缩减版,仅展示与本文相关的部分。

原始代码

static bool getCriterion(float** posAll, int* typesAll, int n, float spatialRange, int targetN) {
	getCriterion_sw.reset();
	// Returns 0 if the cell locations within a subvolume of the total
	// system, comprising approximately targetN cells, are arranged as clusters, and 1 otherwise.
	int i1, i2;
	int nrClose=0; // number of cells that are close (i.e. within a distance of spatialRange)
	float currDist;
	int sameTypeClose=0; // number of cells of the same type, and that are close (i.e. within a distance of spatialRange)
	int diffTypeClose=0; //number of cells of opposite types, and that are close (i.e. within a distance of spatialRange)
	float** posSubvol=0; // array of all 3 dimensional cell positions in the subcube
	posSubvol = new float*[n];
	int typesSubvol[n];
	float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2;
	int nrCellsSubVol = 0;

	// the locations of all cells within the subvolume are copied to array PosSubvol
	for (i1 = 0; i1 < n; i1++) {
		posSubvol[i1] = new float[3];
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]
			-0.5)<subVolMax)) {
			posSubvol[nrCellsSubVol][0] = posAll[i1][0];
			posSubvol[nrCellsSubVol][1] = posAll[i1][1];
			posSubvol[nrCellsSubVol][2] = posAll[i1][2];
			typesSubvol[nrCellsSubVol] = typesAll[i1];
			nrCellsSubVol++;
		}
	}

[section of truncated code]

	for (i1 = 0; i1 < nrCellsSubVol; i1++) {
		for (i2 = i1+1; i2 < nrCellsSubVol; i2++) {
			currDist = getL2Distance(posSubvol[i1][0],posSubvol[i1][1],posSubvol[i1][2],posSubvol[i2][0], posSubvol[i2][1],posSubvol[i2][2]);
		if (currDist<spatialRange) {
			nrClose++;
			if (typesSubvol[i1]*typesSubvol[i2]<0) {
				diffTypeClose++;
			} else {
				sameTypeClose++;
			}
		}
	}

[section of truncated code]

}

修改的代码行: 9, 10, 32-39

优化后的代码

static bool getCriterion(float posAll[][3], int* typesAll, int n, float spatialRange, int targetN) {
	getCriterion_sw.reset();
	// Returns 0 if the cell locations within a subvolume of the total
	// system, comprising approximately targetN cells, are arranged as clusters, and 1 otherwise.
	int i1, i2;
	int nrClose=0; // number of cells that are close (i.e. within a  distance of spatialRange)
	int sameTypeClose=0; // number of cells of the same type, and that are  close (i.e. within a distance of spatialRange)
	int diffTypeClose=0; // number of cells of opposite types, and that are  close (i.e. within a distance of spatialRange)
	float posSubvol[n][3] __attribute__((aligned(64)));
	int typesSubvol[n] __attribute__((aligned(64)));
	const float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2;
	int nrCellsSubVol = 0;

	// the locations of all cells within the subvolume are copied to array  posSubvol

	for (i1 = 0; i1 < n; ++i1) {
		__assume_aligned((float*)posAll, 64);
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]
			-0.5)<subVolMax)) {
			__assume_aligned((float*)posSubvol[nrCellsSubVol], 64);
			__assume_aligned((float*)typesAll, 64);
			__assume_aligned((int*)typesSubvol, 64);
			posSubvol[nrCellsSubVol][0] = posAll[i1][0];
			posSubvol[nrCellsSubVol][1] = posAll[i1][1];
			posSubvol[nrCellsSubVol][2] = posAll[i1][2];
			typesSubvol[nrCellsSubVol] = typesAll[i1];
			++nrCellsSubVol;
		}
	}

[section of truncated code]

	omp_set_num_threads(240);
	#pragma omp parallel for schedule(static)
	reduction(+:nrClose,diffTypeClose,sameTypeClose) private(i1,i2)
	for (i1 = 0; i1 < nrCellsSubVol; ++i1) {
		for (i2 = i1+1; i2 < nrCellsSubVol; ++i2) {
				if (getL2Distance(posSubvol[i1],posSubvol[i2])<spatialRange) {
					++nrClose;
					(typesSubvol[i1]*typesSubvol[i2]<0) ? ++diffTypeClose: ++sameTypeClose;
				}
		}
	}

[section of truncated code]

}

修改的代码行: 1, 9-11, 17, 20-22, 33-35, 38-40

优化说明

该函数的可优化部分包括变量声明和初始化。 第一个循环的可优化性较小,但优化第二个循环意义重大。

该函数的具体优化方式与其他函数相同。

  • 输入阵列的维度在标头中规定。
  • 尽量使用常量。
  • 使用减法在第二个 ‘for loop’ 上实现 OpenMP 并行化。

如欲进一步优化,可以对第一个循环的 if 条件进行并行化处理。另外,扁平化 posAll 和 posSubVol 阵列可以加快执行速度。

主 – 变量声明

主代码较大,因此本文将其分成两部分。这一部分主要处理代码以及面向变量声明优化后的代码。

原始代码

int i,c,d;
int i1, i2, i3, i4;
float energy; // value that quantifies the quality of the cell clustering   output. The smaller this value, the better the clustering
float** posAll=0; // array of all 3 dimensional cell positions
posAll = new float*[finalNumberCells];
float** currMov=0; // array of all 3 dimensional cell movements at the last  time point
currMov = new float*[finalNumberCells]; // array of all cell movements in the  last time step
float zeroFloat = 0.0;
float pathTraveled[finalNumberCells]; // array keeping track of length of path traveled until cell divides
int numberDivisions[finalNumberCells]; //array keeping track of number of  division a cell has undergone
int typesAll[finalNumberCells]; // array specifying cell type (+1 or -1)

修改的代码行: 2, 4-7, 

优化后的代码

int i,c,d,i1;
float energy; // value that quantifies the quality of the cell clustering output. The smaller this value, the better the clustering
float posAll[finalNumberCells][3] __attribute__((aligned(64)));
float currMov[finalNumberCells][3] __attribute__((aligned(64))); //array of
  // all cell movements in the last time step
float pathTraveled[finalNumberCells] __attribute__((aligned(64))); // array
  // keeping track of length of path traveled until cell divides
int numberDivisions[finalNumberCells] __attribute__((aligned(64))); //array
  // keeping track of number of division a cell has undergone
int typesAll[finalNumberCells] __attribute__((aligned(64))); // array
  // specifying cell type (+1 or -1)
float Conc[2][L][L][L] __attribute__((aligned(64)));

修改的代码行: 3-12

优化说明

这部分代码的优化方式有多种。

  • 避免使用‘new’构造函数。
  • 删除无用变量(比如 zeroFloat)。
  • 如果可以声明大小已知的静态阵列,避免使用指示器。
  • 内存对齐;尽管可以使用更多内存,但内存访问速度更快。
  • 在初始化阶段定义变量;这比之后定义该值的速度更快。

主 – 变量初始化

主代码较大,因此本文将其分成两部分。 这一部分主要处理代码以及面向变量初始化优化后的代码。变量的大小和值在该代码中声明,包括最大的阵列 currMov、posAll、pathTraveled 和 Conc。

原始代码

// Initialization of the various arrays
for (i1 = 0; i1 < finalNumberCells; i1++) {
	currMov[i1] = new float[3];
	posAll[i1] = new float[3];
	pathTraveled[i1] = zeroFloat;
	pathTraveled[i1] = 0;
	for (i2 = 0; i2 < 3; i2++) {
		currMov[i1][i2] = zeroFloat;
		posAll[i1][i2] = 0.5;
	}
}
// create 3D concentration matrix
float**** Conc;
Conc = new float***[L];
for (i1 = 0; i1 < 2; i1++) {
	Conc[i1] = new float**[L];
	for (i2 = 0; i2 < L; i2++) {
		Conc[i1][i2] = new float*[L];
		for (i3 = 0; i3 < L; i3++) {
			Conc[i1][i2][i3] = new float[L];
			for (i4 = 0; i4 < L; i4++) {
				Conc[i1][i2][i3][i4] = zeroFloat;
			}
		}
	}
}

修改的代码行: 3-5, 7-10, 13, 14, 16, 18, 20, 22

优化后的代码

// Initialization of the various arrays
omp_set_num_threads(240);
#pragma omp parallel
{
	#pragma omp for simd schedule (static) private (i1) nowait
	for (i1 = 0; i1 < finalNumberCells; ++i1) {
		__assume_aligned((float*)posAll, 64);
		__assume_aligned((float*)currMov[i1], 64);
		__assume_aligned((float*)pathTraveled, 64);
		currMov[i1][0:3]=0;
		posAll[i1][0]=0.5;
		posAll[i1][1]=0.5;
		posAll[i1][2]=0.5;
		pathTraveled[i1] = 0;
	}
	#pragma omp for schedule (static) private (i1,c,d) collapse(3)
	for (i1 = 0; i1 < 2; ++i1) {
		for (c = 0; c < L; ++c) {
			for (d = 0; d < L; ++d) {
				Conc[i1][c][d][0:L]=0;
			}
		}
	}
}

修改的代码行: 2-5, 7-13, 16, 20

优化说明

这部分代码的优化方法有三种。 最基本的方法是将 for 循环的数量从 4 减至 3。 另外两种方法是内存对齐和借助 SIMD 实现 OpenMP 线程并行化。

主 – 阶段 1

主代码较大,因此本文将其分成两部分。 这一部分处理前几个模拟步骤。

原始代码

// Phase 1: Cells move randomly and divide until final number of cells is reached
while (n<finalNumberCells) {
	produceSubstances(Conc, posAll, typesAll, L, n); // Cells produce
	// substances. Depending on the cell type, one of the two substances is produced.
	runDiffusionStep(Conc, L, D); // Simulation of substance diffusion
	runDecayStep(Conc, L, mu);
	n = cellMovementAndDuplication(posAll, pathTraveled, typesAll, numberDivisions, pathThreshold, divThreshold, n);

	for (c=0; c<n; c++) {
		// boundary conditions
		for (d=0; d<3; d++) {
			if (posAll[c][d]<0) {posAll[c][d]=0;}
			if (posAll[c][d]>1) {posAll[c][d]=1;}
		}
	}
}

修改的代码行: 4, 6, 7, 14

优化后的代码

// Phase 1: Cells move randomly and divide until final number of cells is reached
while (n<finalNumberCells) {
	produceSubstances(L,Conc, posAll, typesAll, n); // Cells produce substances. Depending on the cell type, one of the two substances is produced.
	runDiffusionStep(L,Conc, D); // Simulation of substance diffusion
	runDecayStep(L,Conc, mu);
	n = cellMovementAndDuplication(posAll, pathTraveled, typesAll, numberDivisions, pathThreshold, divThreshold, n);
	omp_set_num_threads(240);
	#pragma omp parallel for simd schedule (static) private (c,d) if (n>500)
	for (c=0; c<n; ++c) {
		// boundary conditions
		for (d=0; d<3; d++) {
			if (posAll[c][d]<0) {
				posAll[c][d]=0;
			}else if (posAll[c][d]>1) posAll[c][d]=1;
		}
	}
}

修改的代码行: 4-6, 8, 9, 15

优化说明

用于优化这部分代码的方法有两种: 借助 SIMD 实现 OpenMP 线程并行化和减少 if 子句。

因为目录是静态的,所以借助 SIMD 实现 OpenMP 线程并行化非常实用。另外我们知道,循环大小和每次迭代的成本是一样的,因此所有线程的负载成本都是相同的。 if n>500 条件可帮助降低前 500 个 n 值的额外开销。

原始代码通常执行两次校验。 优化后的代码将 if 校验次数降为一次,只在需要时执行第二次 if 校验。这样可以减少对比的总次数。

主 – 阶段 2 结束

原始代码

for (c=0; c<n; c++) {
	posAll[c][0] = posAll[c][0]+currMov[c][0];
	posAll[c][1] = posAll[c][1]+currMov[c][1];
	posAll[c][2] = posAll[c][2]+currMov[c][2];

	// boundary conditions: cells can not move out of the cube [0,1]^3
	for (d=0; d<3; d++) {
		if (posAll[c][d]<0) {posAll[c][d]=0;}
		if (posAll[c][d]>1) {posAll[c][d]=1;}
	}
}

修改的代码行:  2-4, 9

优化后的代码

#pragma omp parallel for simd schedule (static) private (c,d) if (n>500)
for (c=0; c<n; ++c) {
	__assume_aligned((float*)posAll, 64);
	__assume_aligned((float*)currMov[c], 64);
	posAll[c][0:3] += currMov[c][0:3];
	// boundary conditions: cells can not move out of the cube [0,1]^3
	for (d=0; d<3; d++) {
		if (posAll[c][d]<0) {
			posAll[c][d]=0;
		}else if (posAll[c][d]>1) posAll[c][d]=1;
	}
}

修改的代码行: 1, 3-5, 10

优化说明

这最后一部分代码与阶段 1 的优化方式相同: 借助 SIMD 实现 OpenMP 线程并行化和减少 if 子句。

因为目录是静态的,所以借助 SIMD 实现 OpenMP 线程并行化非常实用。另外我们知道,循环大小和每次迭代的成本是一样的,因此所有线程的负载成本都是相同的。 if n>500 条件可帮助降低前 500 个 n 值的额外开销。

原始代码通常执行两次校验。 优化后的代码将 if 校验次数降为一次,只在需要时执行第二次 if 校验。 这样可以减少对比的总次数。

其他优化

除上述代码各部分列出的优化方法外,还有许多其他优化方法尚未探讨:

  • 将所有后增量定义为前增量。 后增量拷贝实际变量,然后对其进行增量处理。 前增量不进行拷贝,所以速度更快。 只有当增量的点不影响结果时,才可以实现这一性能提升。
  • 在所有阵列上实现内存对齐。 如果阵列对齐,内存操作的速度更快,而且 CPU 无需标记或转换数据。

被否定的优化技巧

在开发最后一部分代码时,我试验了其他并行化和矢量化技巧,比如英特尔 Cilk Plus、英特尔® 线程构建模块(英特尔® TBB),以及英特尔 MKL 中的部分数学线程优化函数。 使用英特尔 Cilk Plus 会降低代码的速度,尽管造成这种结果的原因是我不完全了解如何使用英特尔 Cilk 函数。

由于 OpenMP 函数的表现非常好,我没有使用英特尔 TBB 函数进行其他测试,因此英特尔 TBB 可能也能够提升性能。

另外还可以使用英特尔 MKL 函数,它在处理大量数据时表现良好。 尽管代码移动大量数据,但即时数据的量非常小。 由于即时数据量小(以及其他原因),采用英特尔 MKL 函数进行优化并不十分可行。

关于作者

 

Daniel Vea Falguera 是电子系统工程专业的学生,也是一名创业者,在电子与计算机编程方面拥有浓厚的兴趣和深厚的功底。

链接

实施的大部分代码优化均基于文章《英特尔至强融核协处理器》:https://software.intel.com/zh-cn/articles/optimization-and-performance-tuning-for-intel-xeon-phi-coprocessors-part-1-optimization

如欲了解其他评论过的选项和解决方案,请访问英特尔并行架构现代代码论坛:

https://software.intel.com/zh-cn/forums/intel-moderncode-for-parallel-architectures

英特尔® MKL:https://software.intel.com/zh-cn/mkl-reference-manual-for-c

英特尔® TBB:https://www.threadingbuildingblocks.org/

英特尔® Cilk Plus:https://www.cilkplus.org/

OpenMP*: http://openmp.org/wp/

 

了解面向三维同性有限差分 (3DFD) 波动方程代码的 NUMA

$
0
0

本文将介绍一些技巧,帮助软件开发人员识别并修复使用最新英特尔软件开发工具时遇到的与 NUMA 相关的应用性能问题。

快速链接

1. 简介

非一致性内存访问 (NUMA) 是一种用于多处理的计算机内存设计,在多处理中,内存访问时间取决于与处理器相关的内存位置。根据 NUMA,处理器访问本地内存的速度要快于访问远程内存(位于其他处理器上的内存或处理器之间共享的内存)。

本文将简要介绍如何使用英特尔® VTune™ Amplifier XE 的最新内存访问特性,以识别应用中与 NUMA 相关的问题。 英特尔® 开发人员专区 (IDZ) 曾发表了一篇文章,文章对比了运行于英特尔® 至强™ 处理器和英特尔® 至强融核™ 协处理器的同性三维有限差分应用的开发过程和性能,本文是该文章的进一步延伸。 我们还就源代码修改提出了几点建议,以便 NUMA 环境中的应用实现一致的卓越性能。

我们这里仅探讨面向英特尔® 至强™ 处理器优化的版本,以便着重解决 NUMA 问题。 如欲下载代码,请点击此处。就本文来说,我们将使用为 ISO3DFD 应用提供的源代码的 dev06 版本来对比重要指标,并了解在应用中引进 NUMA 感知的优势。

 

2. 编译和执行 ISO3DFD 应用的步骤

可使用以下 makefile1编译应用:

make build version=dev06 simd=avx2

可使用和源一同提供的 run_on_xeon.pl 脚本运行该应用:

./run_on_xeon.pl executable_name n1 n2 n3 nb_iter n1_block \
 	n2_block n3_block kmp_affinity nb_threads 
 
where
        -executable_name: The executable name
        -n1: N1 //X-Dimension
         -n2: N2 //Y-Dimension
        -n3: N3   //Z-Dimension
        -nb_iter: The number of iterations
        -n1_block: size of the cache block in x dimension
        -n2_block: size of the cache block in y dimension
        -n3_block: size of the cache block in z dimension
        -kmp_affinity: The thread partitionning
        -nb_threads: The number of OpenMP threads

 

3. 识别与 NUMA 相关的性能问题

当代 NUMA 架构非常复杂。 研究内存访问之前,验证 NUMA 是否会影响应用性能将非常有帮助。 这一点可以通过 numactl2实用程序来实现。 验证后,必须跟踪延迟较高的内存访问。 优化这些内存访问可以进一步提升性能。

3.1 numactl

如要确定应用是否会受到 NUMA 的影响,最快的一种方法是完全在单路/NUMA 节点之外运行该应用,然后将其与在多个 NUMA 节点上运行时的性能进行比较。在不受 NUMA 影响的理想场景中,应该可以在插槽范围内顺利扩展,而且,如果没有其他影响扩展的因素,单路性能应该能够提升两倍(相当于使用双路系统)。 如下所示,就 ISO3DFD 来说,单路性能要优于全节点,因此应用性能会受到 NUMA 的影响。尽管这种方法能够帮助我们确定 NUMA 是否会影响应用性能,但它无法帮助我们查明问题所在的确切位置。我们可以使用英特尔 VTune Amplifier XE 的内存访问分析特性详细研究 NUMA 问题。

- 未使用 numactl 时的双路性能(每路 22 个线程):

./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe 448 2016 1056 10 448 24 96 compact 44

n1=448 n2=2016 n3=1056 nreps=10 num_threads=44 HALF_LENGTH=8
n1_thrd_block=448 n2_thrd_block=24 n3_thrd_block=96
allocating prev, next and vel: total 10914.8 Mbytes
------------------------------
time:           3.25 sec
throughput:  2765.70 MPoints/s
flops:        168.71 GFlops

- 使用 numactl 时的单路性能:

numactl -m 0 -c 0 ./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe 448 2016 1056 10 448 24 96 compact 22

n1=448 n2=2016 n3=1056 nreps=10 num_threads=22 HALF_LENGTH=8
n1_thrd_block=448 n2_thrd_block=24 n3_thrd_block=96
allocating prev, next and vel: total 10914.8 Mbytes
-------------------------------
time:           3.05 sec
throughput:  2948.22 MPoints/s
flops:        179.84 GFlops

- 在系统上运行两条进程,每条进程锁定一个插槽(每路 22 个线程):

numactl -c 0 -m 0 ./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe \
	448 2016 1056 10 448 24 96 compact 22 & \
numactl -c 1 -m 1 ./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe \
	448 2016 1056 10 448 24 96 compact 22 &

n1=448 n2=2016 n3=1056 nreps=10 num_threads=22 HALF_LENGTH=8
n1_thrd_block=448 n2_thrd_block=24 n3_thrd_block=96
allocating prev, next and vel: total 10914.8 Mbytes
-------------------------------
time:           2.98 sec
throughput:  2996.78 MPoints/s
flops:        180.08 GFlops
n1=448 n2=2016 n3=1056 nreps=10 num_threads=22 HALF_LENGTH=8
n1_thrd_block=448 n2_thrd_block=24 n3_thrd_block=96
allocating prev, next and vel: total 10914.8 Mbytes
-------------------------------
time:           3.02 sec
throughput:  2951.22 MPoints/s
flops:        179.91 GFlops

 

3.2 使用英特尔® VTune™ Amplifier XE - 内存访问分析

在支持 NUMA 的处理器中,不仅需要研究运行中 CPU 的高速缓存失误,还要研究针对另一 CPU 的远程 DRAM 和高速缓存的引用。为了获取有关这些详情的洞察,我们在应用上运行内存访问分析,如下所示:

amplxe-cl -c memory-access –knob analyze-mem-objects=true \
     -knob mem-object-size-min-thres=1024  -data-limit=0 \
     -r ISO_dev06_MA_10 ./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe \
     448 2016 1056 10 448 24 96 compact 44

以下几个指标与 NUMA 相关:

3.2.1 Memory Bound – 该应用是否受内存限制? 如果受限,带宽利用率直方图是否显示较高的 DRAM 带宽利用率? 因为实际计算密集型工作在插槽之间平分,因此插槽之间的带宽利用率必须达到平衡。

我们可以在 Summary窗口中确定应用是否受内存限制。

图 1:内存受限指标和 DRAM 带宽直方图

请注意,Memory Bound指标非常高,并且十分突出,而 DRAM bandwidth utilization处于中低水平,这不是我们期望的结果,需要进一步调查研究。

3.2.2 英特尔® 快速通道互联(英特尔® QPI)带宽。应用性能有时还会受到插槽间英特尔 QPI 链路带宽的限制。 英特尔 VTune Amplifier 提供识别导致这类带宽问题的根源和内存对象的机制。

Summary 窗口中,使用 Bandwidth Utilization Histogram并选择 Bandwidth Domain下拉菜单中的 QPI

图 2:英特尔® 快速通道互联带宽利用率直方图。

您还可以切换至自下而上视图,并在时间线视图中选择 QPI 带宽利用率较高的区域,并通过该选择进行过滤。

图 3:带宽利用率时间线视图

使用过滤后,时间线图显示仅一个插槽使用了 DRAM 带宽,而 QPI 带宽高达 38 GB/秒。

在相同的自下而上视图中,时间线面板下方的网格显示了该时间范围内所执行的内容。为了查看导致高英特尔 QPI 流量的函数名称,我们从下拉菜单中选择分组至 Bandwidth Domain / Bandwidth Utilization Type / Function / Call Stack,然后展开 High利用率的 QPI带宽域。

图 4:高英特尔® 快速通道互联带宽 - 自下而上视网格图。

NUMA 机器在单个插槽上为 OpenMP* 线程分配内存,而线程分散至各个插槽,因此这些是此类机器遭遇的常见问题。 这样迫使部分线程通过英特尔 QPI 从远程 DRAM 或远程高速缓存加载数据,因此速度比访问本地内存慢得多。

 

4. 修改代码以减少远程内存访问

为了降低 NUMA 的影响,在插槽上运行的线程应该访问本地内存,从而降低英特尔 QPI 流量。这可以使用首次接触策略。在 Linux* 上,内存页面基于首次访问分配;即数据只有在首次写入时才在内存中以物理的形式映射。这样有利于接触数据的线程接近供其运行的 CPU。为达到该目的,必须使用相同的 OpenMP 循环命令将内存初始化成用于计算的内存。考虑到这点,src/dev06/iso-3dfd_main.cc中的初始化函数(包含在 ISO3DFD 源代码中)需要替换成支持首次接触的 initialize_FT。结果,线程将很可能访问并初始化本地内存中用于 iso_3dfd_it函数(传播计算密集型地震波)的相同数据块。此外,我们将静态 OpenMP 调度用于初始化和计算,以进一步提高性能。

void initialize_FT(float* ptr_prev, float* ptr_next, float* ptr_vel, Parameters* p, size_t nbytes, int n1_Tblock, int n2_Tblock, int n3_Tblock, int nThreads){

        #pragma omp parallel num_threads(nThreads) default(shared)
        {
                float *ptr_line_next, *ptr_line_prev, *ptr_line_vel;
                int n3End = p->n3;
                int n2End = p->n2;
                int n1End = p->n1;
                int ixEnd, iyEnd, izEnd;
                int dimn1n2 = p->n1 * p->n2;
                int n1 = p->n1;
                #pragma omp for schedule(static) collapse(3)
                for(int bz=0; bz<n3End; bz+=n3_Tblock){
                        for(int by=0; by<n2End; by+=n2_Tblock){
                                for(int bx=0; bx<n1End; bx+=n1_Tblock){
                                        izEnd = MIN(bz+n3_Tblock, n3End);
                                        iyEnd = MIN(by+n2_Tblock, n2End);
                                        ixEnd = MIN(n1_Tblock, n1End-bx);

                                        for(int iz=bz; iz<izEnd; iz++) {
                                                for(int iy=by; iy<iyEnd; iy++) {
                                                       ptr_line_next = &ptr_next[iz*dimn1n2 + iy*n1 + bx];
                                                        ptr_line_prev = &ptr_prev[iz*dimn1n2 + iy*n1 + bx];
                                                        ptr_line_vel = &ptr_vel[iz*dimn1n2 + iy*n1 + bx];

                                                        #pragma ivdep
                                                        for(int ix=0; ix<ixEnd; ix++) {

                                                                ptr_line_prev[ix] = 0.0f;
                                                                ptr_line_next[ix] = 0.0f;
                                                                ptr_line_vel[ix] = 2250000.0f*DT*DT;//Integration of the v² and dt² here
                                                        }
                                                }
                                        }
                                }
                        }
                }
        }
       
        float val = 1.f;
        for(int s=5; s>=0; s--){
                for(int i=p->n3/2-s; i<p->n3/2+s;i++){
                        for(int j=p->n2/4-s; j<p->n2/4+s;j++){
                                for(int k=p->n1/4-s; k<p->n1/4+s;k++){
                                        ptr_prev[i*p->n1*p->n2 + j*p->n1 + k] = val;
                                }
                        }
                }
                val *= 10;
       }
}

 

5. 针对修改版的内存访问分析

我们在此感兴趣的指标包括 DRAM 带宽利用率和 QPI 带宽。

图 5:内存受限指标和 DRAM 带宽利用率 - 修改版。

通过 Summary 窗口我们可以看出,应用仍然受限于内存,且带宽利用率非常高。

使用 Bandwidth Utilization Histogram,并在 Bandwidth Domain下拉菜单中选择 QPI后,可以看出 QPI 带宽已降低至中低水平。

图 6:采用首次接触策略的 QPI 带宽直方图

切换至自下而上视图并查看时间线,我们可以看出,DRAM 带宽利用率已达到平衡状态或平分于两个插槽,且 QPI 流量降低了 3 倍。

图 7:QPI 流量降低且各插槽 DRAM 带宽达到平衡

 

6. 整体性能对比

我们运行修改版应用,如下所示:

./run_on_xeon.pl bin/iso3dfd_dev06_cpu_avx2.exe 448 2016 1056 \
         10 448 24 96 compact 44                            

n1=448 n2=2016 n3=1056 nreps=10 num_threads=44 HALF_LENGTH=8
n1_thrd_block=448 n2_thrd_block=24 n3_thrd_block=96
allocating prev, next and vel: total 11694.4 Mbytes
-------------------------------
time:           1.70 sec
throughput:  5682.07 MPoints/s
flops:        346.61 GFlops

凭借更出色的内存访问特性,应用吞吐率现已从 2765 MPoints/秒提高到 5682 MPoints/秒,加快了 2 倍。

为了确认这种性能改进是否具有一致性,两种版本的代码运行了 10 次,且每次运行包含 100 次迭代。 为清晰起见,原始 dev06 版代码使用静态和动态 OpenMP 调度运行,以区分换成 OpenMP 调度与引进首次接触策略后分别实现的性能提升。我们可以看出,采用静态 OpenMP 调度运行修改后,NUMA 感知型代码可以实现较高的性能提升。 采用动态调度时,OpenMP 线程和高速缓存数据块(本应用中 OpenMP 线程的工作单位)之间的映射在每次迭代中具有不确定性。 因此,采用动态 OpenMP 调度时,首次接触策略的影响并不显著。

                

图 8:性能差异

 

7. 系统配置

本文图表中提供的性能测试结果基于以下测试系统。 如欲了解更多信息,请访问:http://www.intel.com/performance

组件规格
系统双路服务器
主机处理器英特尔® 至强™ 处理器 E5-2699 V4 @ 2.20 GHz
内核/线程44/44
主机内存64 GB/插槽
编译器英特尔® C++ 编译器版本 16.0.2
分析器英特尔® VTune™ Amplifier XE 2016 Update 2
主机操作系统Linux,版本 3.10.0-327.el7.x86_64

 

8. 参考资料

采用各项同性 (ISO) 的三维有限差分 (3DFD) 代码的八项优化措施 (https://software.intel.com/zh-cn/articles/eight-optimizations-for-3-dimensional-finite-difference-3dfd-code-with-an-isotropic-iso)

英特尔® VTune™ Amplifier XE 2016 (https://software.intel.com/zh-cn/intel-vtune-amplifier-xe)

英特尔® VTune™ Amplifier XE - 解析内存使用数据 (https://software.intel.com/zh-cn/node/544170)

非一致性内存访问 (https://en.wikipedia.org/wiki/Non-uniform_memory_access)

numactl - Linux man 页面 (http://linux.die.net/man/8/numactl)

 

关于作者

Sunny Gogar

Sunny Gogar
软件工程师

Sunny Gogar 获得了佛罗里达大学电子与计算机工程专业的硕士学位,以及印度孟买大学电子与电信学专业的学士学位。目前在英特尔公司软件及服务事业部担任软件工程师。他的兴趣在于面向多核和众核处理器架构的并行编程与优化。

 


[1] 本文对比的所有版本都使用了 -fma 等面向最新英特尔处理器的编译时间标记。

[2]numactl - 控制用于进程或共享内存的 NUMA 策略

 

声明

英特尔技术的特性和优势取决于系统配置,并需要兼容的硬件、软件或需要激活服务。 实际性能会因您使用的具体系统配置的不同而有所差异。 请联系您的系统制造商或零售商,或访问 intel.com,了解更多信息。

本文档不代表英特尔公司或其它机构向任何人明确或隐含地授予任何知识产权。

英特尔明确拒绝所有明确或隐含的担保,包括但不限于对于适销性、特定用途适用性和不侵犯任何权利的隐含担保,以及任何对于履约习惯、交易习惯或贸易惯例的担保。

本文包含尚处于开发阶段的产品、服务和/或流程的信息。 此处提供的所有信息可随时更改,恕不另行通知。 联系您的英特尔代表,了解最新的预测、时间表、规格和路线图。

本文所述的产品和服务可能包含与宣称的规格不符的缺陷或失误。 英特尔提供最新的勘误表备索。

如欲获取本文提及的带订购编号的文档副本,可致电 1-800-548-4725,或访问 www.intel.com/design/literature.htm

英特尔、Intel 标识、Xeon Phi、至强融核、VTune、Xeon 和至强是英特尔在美国和/或其他国家(地区)的商标。

*其他的名称和品牌可能是其他所有者的资产。

英特尔公司 © 2016 年版权所有。


该示例源代码根据英特尔示例源代码许可协议发布。

 

准确预报各种天气:英特尔五步框架帮助实现代码现代化

$
0
0

天气预报是现代生活的一个重要方面,它可在出现恶劣天气状况时即时发出警报,从而帮助有效制定计划和安排物流,并可保护生命财产安全。 但是,准确预测长期的天气情况非常复杂,通常涉及到大量数据集,并且要求对代码进行优化以利用最高级的计算机硬件功能。

创建高性能现代代码的部分挑战来自于全面收集问题域的信息和相关数据集,进而实现并行优化。 开发人员还需要检验现有应用流程和代码,以充分利用系统内的并行硬件资源。

以此为目标,英特尔定义了一个包含五个阶段的多层并行代码现代化框架,为软件开发人员提高应用性能提供了一个系统的方法。 在本案例研究中,我们重点介绍了与矢量化、线程并行性和 I/O 吞吐量最大化相关的代码优化,以及将其应用到“天气研究和预报”模型中的情况,并突出介绍了英特尔® VTune™ 放大器、英特尔® Advisor XE 和英特尔® 至强™ 处理器的重要功能。

从“天气研究和预测”模型中获得的经验教训

现代天气预报主要依赖于数值天气预报系统,这些系统可对大气和海洋的数学模型进行计算机模拟操作。 其中一个主要系统是天气研究和预报 (WRF) 模型,该模型是一款开源解决方案,为全球广泛采用,可用来进行大气研究和日常作业预报。 WRF 模型始建于 20 世纪 90 年代末,随后历经更新,现为全球 150 多个国家的 30,000 个注册用户提供服务。

WRF 模型依据物理学、流体力学和化学定律使用偏微分方程系统进行计算,并将地球划分为三维网格。 这让系统模型与大气条件的交互范围从几十米扩大到数千公里。

问题域的性质使得数值天气预报成为通过并行性和使用其他高级技术实现代码现代化的理想选择。 在本文中,我们重点介绍了英特尔技术如何帮助将最常见的现代化方法应用到 WRF 模型中,并阐释了软件开发人员如何使用类似的方法更广泛地优化更多软件应用。

明确潜在优化

现代高性能处理器采用多种资源,包括多核架构、矢量处理功能、高速缓存和高带宽 I/O 通信。 构建可充分利用这些特性的软件能够让应用更高效地运行,从而能够处理更大的数据集。 这使得之前费用过高或甚至不可能实现的操作和分析得以实现。

英特尔五步代码现代化框架建议开发人员:

  • 使用高级优化工具来描述代码和确认矢量化和线程机会。 在找到潜在热点后,使用一流的编译器和优化的函数库来生成最高效的代码。
  • 确保代码使用足够的精度、类型常量和最佳配置设置,以优化标量和串行操作。
  • 尽可能使用矢量,即在处理器架构中使用多数据 (SIMD) 特性。
  • 执行线程并行化和分析线程扩展可帮助发现线程同步问题或低效的内存访问。
  • 使用分布式内存队列并行化将应用从多核扩展为众核,尤其在应用添加到高度并行的实施中时更应如此操作。

注:关于该五步框架的详细信息,请参阅什么是代码现代化?

应用分析

代码现代化的第一步是分析工作负载,确认要优化代码的候选位置。 借助英特尔® VTune™ 放大器,您可以查看 CPU 性能、线程性能、带宽利用率、高速缓存效率等。 该工具能够以更高级的线程模型来展示信息,从而能够更轻松地阐释结果。

当您准备好进行矢量化和线程化操作时,英特尔® Advisor XE 可帮助确认需要特别关注的循环。 研究表明,进行了矢量化线程化的代码的性能是传统代码的 175 倍或更高,约为仅进行线程化矢量化的代码的 7 倍。

英特尔® Advisor XE 还可帮助发现可能会对矢量化产生影响的内存访问模式,以及包含对矢量化产生阻拦作用的依赖性的循环。

WRF 的模块化架构可帮助进行高级分析,提供自然边界,帮助发现更重要的代码段。 借助手动检查源代码、汇编代码和编译器报告的功能,开发人员可以进一步确定无法自动矢量化并需要更新至原始指令才能够获益的代码段。

利用矢量化

分析完应用后,代码现代化的下一步是对代码进行矢量化处理。 代码进行矢量化后,单个计算指令可以在一维数据数组(即矢量)上同时运行。 一般情况下,这些标量数据通常包括整数和单精度和双精度浮点值。

矢量计算尤其适合应用需要在大型数据集上执行统一运算的情况。 这可显著提升英特尔® 集成众核架构处理器(如英特尔® 至强融核™ 协处理器)的性能。 在 WRF 模型中,矢量化允许将相同的指令同时运用到多个数据元素上,这对于以 stepwise 的方式处理不断变化的大气状况至关重要。

循环是开始进行矢量化的合适位置,英特尔编译器非常擅长内部循环自动矢量化。 但是,您可以让编译器生成矢量化报告。 这可以帮助确认应用中编译器无法进行自动矢量化的位置。 此时,编译器的编译指示和指令可以帮助编译器进行矢量化。

在整个过程中,您还需要确保应用内的控制流不发生意外偏离。 控制数据调整对于优化数据访问和 SIMD 处理器而言都非常重要。

例如,考虑 WRF 模型中的以下代码段:

#pragma simd
DO i=i_start, i_end
   ph_low = ...
   flux_out =   max(0.,fqx (i+1,k,j))-min(0.,fqx (i  ,k,j)) )...
   IF( flux_out .gt. ph_low ) THEN
      scale = max(0.,ph_low/(flux_out+eps))
      IF( fqx (i+1,k,j) .gt. 0.) fqx(i+1,k,j) = scale*fqx(i+1,k,j)
      IF( fqx (i  ,k,j) .lt. 0.) fqx(i  ,k,j) = scale*fqx(i  ,k,j)
   END IF
ENDDO

在本案例中,循环会建议使用能够防止自动矢量化的流和输出依赖性。 这是一个容易证明的实践,事实上,IF 条件意味着没有依赖性,添加 simd 编译提示支持编译器对循环进行矢量化,从而提高矢量化的性能。

WRF 模型可进一步采用以下优化:

优化总结
runscripts至强融核上的亲和性优化,可极大改进负载平衡,并支持大幅减少队列数量。
advect_scalar_pd ysu2d通过添加 SIMD 编译提示和调整指令显著提升矢量化性能。
w_damp删除了异常情况下的某些信息,但仍然对异常状况进行标记,从而提升了矢量化。 通过循环外提 (un-switching) 可能能够获得更出色的矢量化。
WSM5通过调用到 WSM5 中复制到 statically-sized thread-local 贴图阵列内外并进行安排,以便对值进行调整,并使其以 SIMD 宽度(16 字)的数据块组合。 此外,由于条件表达式的原因,重新构建的循环未进行矢量化。
编译指示WRF 代码中的许多循环都从 SIMD/IVDEP 和其他编译指示中获得了优势。

下表展示了采用优化代码前后,面向至强融核进行调整的配置文件:

英特尔® 至强融核™ 协处理器(优化前)
函数应用百分比
OMPTB::TreeBarrierNGO::exitBarrier0.00%
advect_scalar_pd_21.43%
w_damp_13.04%
ysu2d_5.18%
nislfv_rain_plm_5.15%
advance_uv_3.84%
wsm52d_3.62%
advect_scalar_3.29%
set_physical_bc3d_3.19%
advance_w_2.91%
advance_mu_t_2.37%
advect_w_2.02%
OMPTB::TreeBarrier::enterBarrier0.00%
pbl_driver_1.81%
rk_update_scalar_1.55%
horizontal_pressure_gradient_1.46%
rrtm_1.44%
cal_deform_and_div_1.27%
英特尔® 至强融核™ 协处理器(优化后)
函数应用百分比
OMPTB::TreeBarrierNGO::exitBarrier0.00%
advect_scalar_pd7.73%
advance_uv5.81%
advect_scalar5.40%
advance_w4.50%
ysu2d4.03%
advance_mu_t3.64%
nislfv_rain_plm3.14%
rk_update_scalar2.97%
zero_tend2.63%
calc_p_rho2.35%
Rtrn2.32%
wsm52d2.29%
pbl_driver2.28%
cal_deform_and_div2.27%
[vmlinux]0.00%
small_step_prep2.18%
phy_prep2.15%

通过线程并行化进行优化

WRF 模型包含多个模块、功能和程序,它们分别关注天气预报的不同方面,包括风、热传递、太阳辐射、相对湿度和地表水文学等。 这支持使用其他层的优化,即线程并行化,将一个线程的特定任务上的线程作业进行整合,并通过共享内存进行通信。

由于阿姆达尔定律(即并行程序的加速受连续部分的限制),线程并行化最适合用于外层。 这表示,每个线程需要尽可能多地处理任务。 它还表示,您需要让线程并行化程度与目标硬件的内核数匹配,使用过多或过少并行化都可能由于开销问题而影响性能提升。

对于 WRF 模型,OpenMP* (共享存储并行程序)可提供有效的方法。 OpenMP 是一个应用编程接口 (API),支持使用 FORTRAN* 和其他语言进行多平台、共享内存多处理。 OpenMP 的工作原理是使用一个主线程,叉接一系列从线程,然后这些从线程在多个处理器上并行、独立运行。

OpenMP 包括一系列编译器指令、函数库程序和环境变量。 在 WRF 模型中,使用预处理器指令来确认要并行运行的代码段。 当代码并行化完成后,线程会返回主线程,继续像往常一样运行。 借助 OpenMP 模型,WRF 模型能够以最低的开销实现任务和数据的并行化。

WRF 模型还可使用消息传递接口 (MPI)/ OpenMP* 分解,在两个层面:补丁和贴图上运行。 对于补丁而言,Conus12 包含一个 425x300 的范围,划分为多个长方形的块,再分配给 MPI 进程。 补丁可以进一步划分为贴图,这些贴图可以分配给进程内的共享内存线程 (OpenMP)。 贴图在整个 X 维度扩展,然后从 Y 维度上再次划分。 例如,如果采用每补丁 80 x 50 的网格点,那么 X 便是 80,Y 是 50。 对于每队列 3 个 OpenMP,补丁将会再次划分为 3 个贴图: 80x17、80x17、80x16。

这些贴图可在 namelist.inut 指定为 “numtiles” 或使用环境变量(WRF_NUM_TIMES、WRF_NUM_TILES_X 和 WRF_NUM_TILES_Y)。 下图展示了分解的 WRF 模型:

这导致英特尔® 至强融核™ / 英特尔® 至强™ 上呈现如下 WRF.conus12km 布局:

WRF 模型中的 OpenMP 开销(即执行 OpenMP 任务耗费的运行时)可忽略不计,原因如下:

  • 有近 20000 个 fork-join,未使用其他 OpenMP
  • 每 fork-join 有近 50000 个时钟(高)
  • 总共不到 1 秒钟的时间内仅使用 10e8 时钟,不到 1%

请注意,如果添加其他的 MPI 队列,开销可能将会显著增加,其可转换为较小的每队列工作负载。

另一个并行性优化集中在 surface_driver。 考虑一下下面的 init 代码:

! 3d arrays
v_phytmp = 0.
u_phytmp = 0.
! Some 2d arrays

This code was parallelized to the following:

!$OMP PARALLEL DO &
!$OMP PRIVATE (ij, i, j, k)
DO ij = 1,num_tiles
   do j = j_start(ij),j_end(ij)
   do k = kms,kme
   do i = i_start(ij),i_end(ij)
   v_phytmp(i, k, j) = 0
   u_phytmp(i, k, j) = 0

实施这些变更可将串行时间减少到低于采样分辨率(效果为 0)。

数据带宽注意事项

对于数据密集型问题(如 WRF 模型),如果应用尝试使用的带宽高于可提供的量,那么系统就会受到带宽的影响。 当使用高带宽处理器(如英特尔至强融核协处理器)时,其受到的影响较小,但是仍然值得开发人员注意。

另一个要注意的方面是确保数据符合处理器的二级高速缓存,最大限度减少访问主内存的需求,因为这一需求会对性能产生很大的影响。 这可能需要您使用高速缓存分块技术,将较大数据集的子集加载到高速缓存中,并充分利用应用内在的数据重复使用操作。

通过配置实现最佳性能

大家经常会忽略要正确设置相关配置文件和环境变量的需求,但是这一需求对于实现最佳应用性能而言非常有必要。 这可能包括为英特尔® MPI 函数库和英特尔® 编译器提供环境,启动较大文件 I/O 支持,以及将英特尔® 至强融核™ 协处理器堆栈尺寸设置为足够大的值,以防止出现分段故障。

设置正确的编译器选项对于应用性能而言同样至关重要。 面向英特尔至强处理器和英特尔至强融核协处理器构建 WRF 模型,例如,添加以下选项:

  • -mmic: 构建一个可以在英特尔® 至强融核协处理器本地运行的应用程序
  • –openmp: 支持编译器根据 OpenMP* 指令生成多线程代码(与 -fopenmp 相同)
  • -O3: 支持强制优化编译器
  • -opt-streaming-stores always: 生成流存储
  • -fimf-precision=low: 通过低精度实现更高的性能
  • -fimf-domain-exclusion=15: 为单精度和双精度提供最低的精度序列
  • -opt-streaming-cache-evict=0: 关闭所有的高速缓存块清除

英特尔使用英特尔至强融核协处理器通过以下运行时参数发现了更多优化:

  • 据测定,在英特尔至强融核上使用平衡的亲和性可以达到最好的效果。
  • 最佳的贴图方式是内核在 Y 轴上,线程在 X 轴上。
  • 让内核分布在 X 轴上可以提高高速缓存的命中率。
  • 使用新的环境变量,如 WRF_NUM_TILES_X (每核线程数)和 WRF_NUM_TILES_Y (内核数)。 例如,60 个内核,每内核 3 个线程: X=3,Y=60。

下图展示了英特尔至强融核协处理器上的紧凑 (compact)、分散 (scatter) 和平衡 (balanced) 亲和性之间的区别:

结论

一流的高性能处理器,如英特尔至强和英特尔至强融核协处理器,可提供多种功能帮助实现多层并行代码现代化。 这可为开发人员提供多种选项,助其交付出色的应用性能,并支持应用进行扩展和提供强大能力。

如 WRF 模型所示,使用分步实施的方法逐步分析应用,并解决矢量、线程并行化和数据带宽等主要领域的问题可确保为当今和未来提供最佳效果。

更多信息

了解关于“天气研究和预报模型”的更多信息:www.wrf-model.org

阅读什么是代码现代化?一文:software.intel.com/zh-cn/articles/what-is-code-modernization

阅读关于矢量化要素的更多信息: software.intel.com/zh-cn/articles/vectorization-essential

英特尔® 至强融核™ 协处理器和英特尔® 至强™ 处理器上的 WRF Conus12km中了解更多信息:software.intel.com/zh-cn/articles/wrf-conus12km-on-intel-xeon-phi-coprocessors-and-intel-xeon-processors

对称模式下英特尔® 至强融核™ 协处理器和英特尔® 至强™ 处理器上的 WRF Conus2.5km中了解更多信息: software.intel.com/zh-cn/articles/wrf-conus25km-on-intel-xeon-phi-coprocessors-and-intel-xeon-processors-in-symmetric-mode

关于英特尔® VTune™ 放大器的更多信息,请参阅:software.intel.com/zh-cn/intel-vtune-amplifier-xe

关于英特尔® Advisor XE 的更多信息,请参阅:software.intel.com/zh-cn/intel-advisor-xe

*其他的名称和品牌可能是其他所有者的资产。

如何检测 Knights Landing AVX-512 支持(英特尔至强融核处理器)

$
0
0

英特尔至强融核处理器(代号“Knights Landing”)是第二代英特尔至强融核产品的一部分。Knights Landing 支持 AVX-512 指令,特别是 AVX-512F (foundation)、AVX-512CD(冲突检测)、AVX-512ER(指数函数和倒数函数)和 AVX-512PF(预取)。

如果希望应用能够随处运行,为了在程序中使用这些指令,需要确保操作系统和处理器在应用运行时支持这些指令。

英特尔编译器提供的单个函数 _may_i_use_cpu_feature 可轻松处理一切。 该程序显示了我们如何用它来测试是否能够 使用 AVX-512F、AVX-512ER、AVX-512PF 和 AVX-512CD 指令。

#include <immintrin.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
  const unsigned long knl_features =
      (_FEATURE_AVX512F | _FEATURE_AVX512ER |
       _FEATURE_AVX512PF | _FEATURE_AVX512CD );
  if ( _may_i_use_cpu_feature( knl_features ) )
    printf("This CPU supports AVX-512F+CD+ER+PF as introduced in Knights Landing\n");
  else
    printf("This CPU does not support all Knights Landing AVX-512 features\n");
  return 1;
}

如果采用 -xMIC_AVX512 标记进行编译,英特尔编译器将自动保护二进制,无需进行检查。例如,如果按照以下方式编译和运行,我们可以看到机器(并非 Knights Landing)的运行结果。

icc -xMIC-AVX512 -o sample sample.c
./sample

请验证操作系统和处理器是否支持英特尔 MOVBE、F16C、AVX、FMA、BMI、LZCNT、AVX2、AVX512F、ADX、RDSEED、AVX512ER、AVX512PF 和 AVX512CD 指令。


为了在所有处理器上运行,我们按照以下方式编译和运行:

icc -axMIC-AVX512 -o sample sample.c
./sample

在 Knights Landing 上运行时,将显示:
This CPU supports AVX-512F+CD+ER+PF as introduced in Knights Landing

在不支持 AVX-512 指令,但至少相当于 Knights Landing 的处理器上运行时,将显示:
该 CPU 不支持所有 Knights Landing AVX-512 特性

如果我们希望支持除英特尔之外的编译器,代码将稍微复杂一些,因为函数 _may_i_use_cpu_feature 不是标准函数(而且两个都不是 gcc 和 clang/LLVM 的 __buildin 函数)。以下代码至少适用于英特尔编译器、gcc、clang/LLVM 和 Microsoft 编译器。

#if defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 1300)

#include <immintrin.h>

int has_intel_knl_features()
{
  const unsigned long knl_features =
      (_FEATURE_AVX512F | _FEATURE_AVX512ER |
       _FEATURE_AVX512PF | _FEATURE_AVX512CD );
  return _may_i_use_cpu_feature( knl_features );
}

#else /* non-Intel compiler */

#include <stdint.h>
#if defined(_MSC_VER)
#include <intrin.h>
#endif

void run_cpuid(uint32_t eax, uint32_t ecx, uint32_t* abcd)
{
#if defined(_MSC_VER)
  __cpuidex(abcd, eax, ecx);
#else
  uint32_t ebx, edx;
 #if defined( __i386__ ) && defined ( __PIC__ )
  /* in case of PIC under 32-bit EBX cannot be clobbered */
  __asm__ ( "movl %%ebx, %%edi \n\t cpuid \n\t xchgl %%ebx, %%edi" : "=D" (ebx),
 # else
  __asm__ ( "cpuid" : "+b" (ebx),
 # endif"+a" (eax), "+c" (ecx), "=d" (edx) );
	    abcd[0] = eax; abcd[1] = ebx; abcd[2] = ecx; abcd[3] = edx;
#endif
}

int check_xcr0_zmm() {
  uint32_t xcr0;
  uint32_t zmm_ymm_xmm = (7 << 5) | (1 << 2) | (1 << 1);
#if defined(_MSC_VER)
  xcr0 = (uint32_t)_xgetbv(0);  /* min VS2010 SP1 compiler is required */
#else
  __asm__ ("xgetbv" : "=a" (xcr0) : "c" (0) : "%edx" );
#endif
  return ((xcr0 & zmm_ymm_xmm) == zmm_ymm_xmm); /* check if xmm, zmm and zmm state are enabled in XCR0 */
}

int has_intel_knl_features() {
  uint32_t abcd[4];
  uint32_t osxsave_mask = (1 << 27); // OSX.
  uint32_t avx2_bmi12_mask = (1 << 16) | // AVX-512F
                             (1 << 26) | // AVX-512PF
                             (1 << 27) | // AVX-512ER
                             (1 << 28);  // AVX-512CD
  run_cpuid( 1, 0, abcd );
  // step 1 - must ensure OS supports extended processor state management
  if ( (abcd[2] & osxsave_mask) != osxsave_mask )
    return 0;
  // step 2 - must ensure OS supports ZMM registers (and YMM, and XMM)
  if ( ! check_xcr0_zmm() )
    return 0;

  return 1;
}
#endif /* non-Intel compiler */

static int can_use_intel_knl_features() {
  static int knl_features_available = -1;
  /* test is performed once */
  if (knl_features_available < 0 )
    knl_features_available = has_intel_knl_features();
  return knl_features_available;
}

#include <stdio.h>

int main(int argc, char *argv[]) {
  if ( can_use_intel_knl_features() )
    printf("This CPU supports AVX-512F+CD+ER+PF as introduced in Knights Landing\n");
  else
    printf("This CPU does not support all Knights Landing AVX-512 features\n");
  return 1;
}

致谢:感谢 Max Locktyukhin(英特尔)的文章 -“如何检测第四代智能英特尔酷睿处理器家族的新指令支持”,我将其用作 Knights Landing 检测代码的模型。

Viewing all 49 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>