澳门新葡亰网址下载如何编写可以自动矢量化的代码

by admin on 2020年3月22日

Java Vector API 意在提供二个编写制定用来在 Java
中编辑复杂的向量(vetor卡塔尔(قطر‎算法。

可以活动矢量化的代码


利用gcc version 4.3.4的gcc编写翻译器,编写翻译下边包车型客车演示代码:

int main(){

  const unsigned int ArraySize = 10000000;

  float* a = new float[ArraySize];

  float* b = new float[ArraySize];

  float* c = new float[ArraySize];

  for (unsigned int j = 0; j< 200 ; j++) // some repetitions

    for ( unsigned int i = 0; i < ArraySize; ++ i)

        c[i] = a[i] * b[i];

}

编写翻译并运维方面包车型客车代码:

g++ vectorisable_example.cpp -o vectorisable_example -O2 time ./vectorisable_example

简轻易单,今后大家看下如何告诉 compiler 自动对代码举办矢量化:

g++ vectorisable_example.cpp -o vectorisable_example_vect -O2 -ftree-vectorize time ./vectorisable_example_vect

第二种编写翻译格局,发生的可执路程序比第一种发生的可执路程序运转的越来越快?大家浓烈钻研下,看看为何他就疑似此快?

原来的作品链接澳门新葡亰网址下载
 下载文件 

燕书和英特尔正在开辟一种 Java API 来为平台丰盛对向量(vector卡塔尔或Graal编写翻译器,项指标靶子包蕴:

简介


从上世纪七十时代早先时期带头,AMD就已经将单指令多多少(SIMD)机器指令集成到其商品CPU产品线中。那个SIMD指令允许贰回将同一的数学运算(乘,加…)应用于2,4或以至越多的数据值上。那组数据值也被称呼“vector”(不要与代数中的vector混淆)。理论上矢量计算的性质增益等于CPU能够宽容的矢量单位的数目。

就算SIMD指令集已经济合营龙到何奇之有CPU中一定长一段时间了,不过那些增加功效(SIMD指令集)只可以通过在C恐怕C++代码中经过准汇编语言进行利用。像GNU编写翻译器种类(GCC)那样的现世编写翻译器现在得以将寻常的C或C
++源代码转变来向量操作。那一个进程被叫作自动矢量化(auto-vectorization)。
那篇小说,我们将会介绍要求的软件工具和编制程序技能,并进而提供使用GCC自动矢量化功效的示例源代码

1. 简介

本学科将介绍两种优化利用,以支撑其在AMD® 至强融核™ 微处理器上运转。
本教程中的优化流程分为四个部分:

  • 第一片段介绍用于对代码进行矢量化(数据并行化)管理的通用优化技巧。
  • 其次片段介绍如何添加线程层并行化,以丰盛利用场理器中的全数可用内核。
  • 其三局地将因而在Intel至强融核微型机上启用内部存款和储蓄器优化,以优化代码。

末尾的定论部分将通过图形格局呈现各优化步骤所完成的质量升高。

优化进程如下:以串行、品质有待升高的示范代码为根底。
然后使用优化技能管理该代码,获得矢量化版本的代码,并对该矢量化代码进行更进一层的线程并行化管理,以使其改为并行版代码。
最后选择英特尔® VTune™ Amplifier
分析该相互代码的内部存款和储蓄器带宽,以应用高带宽内部存储器进一层优化质量。
本教程以附属类小构件方式提供那多少个版本的代码(mySerialApp.cmyVectorizedApp.c 和 myParallelApp.c)。

示范代码为二个流处理利用,带有多少个带有输入和出口的缓冲区。
第五个输入数据集带有一次方程周到。
第贰个出口数据集用于保留种种三次方程式的根。
为简易起见,接纳的周密可确认保证壹次方程式始终求得四个实根。

考虑一回方程式:

澳门新葡亰网址下载 1

八个根为已知公式的解:

澳门新葡亰网址下载 2

求得多个实根的条件是 澳门新葡亰网址下载 3 和 澳门新葡亰网址下载 4

  • 提供清晰简洁的 API,能够抒发各个矢量总计

  • 在 x64 构造上提供保证的周转时编写翻译和品质

  • 合作分歧的结构

  • 高贵降级:假使矢量总括不能够在运维时阶段作为体系完全表明,只怕因为 x64
    构造不帮衬某些指令,或任何 CPU 构造不被辅助,那么 Vector API
    的落实会文雅地贬低,但仍然会起效果。开荒者也会吸收接纳关于此主题材料的警示

硬件


要以己度人自动矢量化代码能够带给多大的习性升高,首先需求明白您正在使用的CPU的力量以至CPU能够拍卖多少个Vector。由此,运营以下命令:

$ cat /proc/cpuinfo…
flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat
pse36 clflush dts acpi mmx fxsr sse sse2ss ht tm pbe syscall
nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl
xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl
vmx est tm2ssse3cx16 xtpr pdcmsse4_1 sse4_2x2apic popcnt
xsaveavxlahf_lm ida arat epb xsaveopt pln pts dts tpr_shadow vnmi
flexpriority ept vpid…

在CPU属性中,您将找到有关SIMD成效的新闻(以粗体优异体现)。若新闻中蕴藏下表列出的字符串,则您的CPU扶植(SIMDState of Qatar功能:

Name Register Size Amount of floats Amount of doubles
mmx (*) 64 bit 0 0
sse2 128 bit 4 2
ssse3 128 bit 4 2
sse4_1 128 bit 4 2
sse4_2 128 bit 4 2
avx 256 bit 8 4
  • AMD计算机上的首先个矢量单元只好容纳七个迭代器。

持有AMD和AMD 63个人CPU都最少扶助sse2指令集。
AVX指令集已于二零一三年推出,选用Sandy Bridge结构,可在新式一代Mac Book
Pro台式机Computer上应用。
AVX指令集能够并且对多个双精度浮点值应用数学生运动算。由此,与未使用矢量单位的版本对照,应用程序能够实现的最大质量增益是4倍。
Vector单位大小的持续增大,反应了是Computer微连串构造演化的新趋向。十年前,计算机的石英钟频率大约年年翻一番。Computer的原子钟频率已经达标了极端,成立商转而初叶提供进一层大的矢量单位,并且在各个微电路中提供更增多的主导数据。

2. 硬件和软件

该程序在预临蓐Intel® 至强融核™ 微处理器(型号 7250,68 个基本,挂钟速度为
1.4 GHz,96 GB DDHighlander4 RAM、16 GB 多通道动态随机存取存款和储蓄器
(MCDRAM卡塔尔国)上运营。 每内核 4 个硬件线程,由此该系统运作时共有 2柒13个硬件线程。 大家在该系统中安装了 Red Hat Enterprise Linux*
7.2、英特尔® 至强融核™ 微处理机软件版 1.3.1 和英特尔® Parallel Studio XE
2015 更新版 3。

设若必要查阅系统的微管理机类型和数码,还可以 /proc/cpuinfo 显示输出。
举例:

澳门新葡亰网址下载 5

测量试验系统的一体化输出呈现了 272 个 CPU 或硬件线程。
请注意,标志字段显示了命令扩张 avx512favx512pfavx512eravx512cd;它们均为AMD至强融核微电脑扶持的授命增添。

还足以运作lscpu,突显有关 CPU 的音信:

澳门新葡亰网址下载 6

以上命令突显系统包罗 1 个插座、68 个水源和 272 个 CPU。 它还呈现该系统有
2 个 NUMA 节点,272 个 CPU 全体归属 NUMA 节点 0。 越来越多关于 NUMA
的新闻敬请参阅 Knights Landing 上的
MCDRAM(高带宽内部存款和储蓄器)简单介绍。

剖判和优化示例程序早前,请编写翻译该程序并运营二进制代码以赢得基准质量。

假定草书和Intel能兑现那一个承诺,Java Vector API 将会提供一种那样的编制—— 利用 HotSpot 设想机中的现成辅助开展矢量化,进而在 Java
中编辑复杂的矢量算法。使用向量运算,一定水平的并行能够在单个 CPU
周期内做到更加的多专门的学问。由此,能够获得断定的习性升高。API 中的客商模型(user
model卡塔尔国将运用底层的矢量硬件,进而使得矢量化更具可预测性。

怎么代码能够被auto-vectorized ?


为了将总结分布到CPU的矢量单元,编写翻译器必得对源代码的依据和相互作用有三个很好的知晓。但最主要的是,编写翻译器必得能够检查实验出那个代码段能够应用SIMD指令张开优化。最简便易行的事态是对数组试行计算的大循环:

for ( unsigned int i = 0; i < ArraySize; ++ i)
{
      c[i] = a[i] * b[i];
}

动用万分如故超越gcc461(在slc5_amd64_gcc461
scram类别构造的服务器中可用State of Qatar版本的gcc编写翻译器,编写翻译的时候,必定要带上is-ftree-vectorize

注意:从机关矢量化中受益最多的大循环是含有数学生运动算的巡回。只是迭代对象集合的循环不会赢利,在好多气象下居然不能够自动矢量化。

能够在那处找到能够自行矢量化的轮回结构列表:http://gcc.gnu.org/projects/tree-ssa/vectorization.html
只要您选拔选用-ftree-vectorizer-verbose =
7
编译你的代码,GCC将会给您多少个有关你的先后中全体循环的详细报告,以致它们是不是曾经被电动矢量化了。以下报告是马到功成对循环实行向量化的结果:

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 1, outside_cost = 0.

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 2, outside_cost = 0.

autovect.cpp:66: note: vect_model_store_cost: aligned.

autovect.cpp:66: note: vect_get_data_access_cost: inside_cost = 3, outside_cost = 0.

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_load_cost: aligned.

autovect.cpp:66: note: vect_model_load_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: vect_model_simple_cost: inside_cost = 1, outside_cost = 1 .

autovect.cpp:66: note: vect_model_store_cost: aligned.

autovect.cpp:66: note: vect_model_store_cost: inside_cost = 1, outside_cost = 0 .

autovect.cpp:66: note: Cost model analysis:

Vector inside of loop cost: 5

Vector outside of loop cost: 11

Scalar iteration cost: 5

Scalar outside cost: 0

prologue iterations: 0

epilogue iterations: 2

Calculated minimum iters for profitability: 3

autovect.cpp:66: note:  Profitability threshold = 3

autovect.cpp:66: note: Profitability threshold is 3 loop iterations.

autovect.cpp:66: note: LOOP VECTORIZED.

若叁个生生不息不可能被向量化, GCC将会付给原因:

autovect.cpp:133: note: not vectorized: control flow in loop.

上边包车型客车输出的含义是:在循环中调用了tostd ::
cout,引进了四个调整流,进而不恐怕对该循环实行向量化。

3. 评测基准代码

伴随依附的顺序 mySerialApp.c 中突显了简短的应用方案奉行。
周到 a、b 和 分成布局 Coefficients 组,根 x1和
x2 分成结构 Roots 组。 周到和根为单精度浮点数。
各种周详元组分别对应四个根元组。
程序将分配N 个周到元组和 N 个根元组。 N 的数值十分的大(N = 512M
成分,精确的话为 512*1024*1024 = 536,870,912 个因素)。
全面结商谈根构造如下所示:

澳门新葡亰网址下载 7

总结程序依照上述公式计算实根 x1 和 x2。
我们还接纳正规连串放大计时器衡量总结时间。
不衡量缓冲区分配时间和开端化时间。 轻松程序将总括流程重复 10 次。

发端时,通过行使英特尔® C++ 编写翻译器编译基准代码,以评测应用的条件质量:

$ icc mySerialApp.c

暗许意况下,编写翻译器依赖开关 -O2(面向最大速度优化)举办编写翻译。
然后运转该采用:

$ ./a.outNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....SERIALElapsed time in msec: 461,222 (after 10 iterations)

出口呈现,系统花费了 461,222 纳秒对数码庞大的条规(N = 512M 成分)进行了
10 次迭代,以流处理多少、总结根并保存结果。
该程序为种种全面元组总计根元组。
注意,该标准代码无法足够利用系统中的多量可用内核或 SIMD
指令,因为它以串行和标量格局运作(每一遍仅二个线程处理二个元组成分)。
由此,仅三个硬件线程 (CPU卡塔尔 处于运维情形,其他 CPU 均处在闲置状态。
你能够行使编写翻译器选项 -qopt-report=5 -qopt-report-phase:vec 生成矢量化报告 (.optrpt*卡塔尔来表明这一点。

$ icc mySerialApp.c -qopt-report=5 -qopt-report-phase:vec

度量基准代码品质后,大家初始对代码进行矢计量化验管理。

在此一端,钟鼓文和AMD表示,该议案并未有引用叁个一定的 Java 版本作为
API 信赖的版本,但该项指标约束仅适用于 Java
SE。此外,该品种设有二个如此的风险 —— 在 x64 结构上,API
也许会偏侧于 SIMD,但是别的布局也将会被构思在内,特别是 ARM Scalar
Vector 增添构造。

让GCC自动对你的代码举行向量化


想要让GCC对友好的代码进行机动向量化,供给动用新型的编写翻译器,建议接收起码GCC
4.6.1
本子的编写翻译器,平常来讲,版本越新越好。

4. 代码矢量化

初藳:开源中华夏儿女民共和国

编写翻译器标示


想要展开auto-vectorization,使用标记:

-ftree-vectorize

倘诺您使用优化品级-O3or实行编写翻译,则隐式启用此选项。
想要获得什么loop是现已被auto-vectorized和怎么着loops未有被成功矢量化以至原因,能够动用选择:

-ftree-vectorizer-verbose=7

关于怎么样阅读那么些输出,请看上面包车型大巴Real-World代码示例部分。
一些循环构造(举个例子浮点数的减法)只好在同意编写翻译器改动数学生运动算顺序的情景下进行矢量化。要变成那一点,须求选拔选拔:

-ffast-math

设若你使用优化等第-Ofast,则会隐式启用该选项。请留意,fast-math选项会改正改善浮点运算中的错误操作。详细情况请看http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Optimize-Options.html
其它,您能够显然建议编写翻译器在转移二进制文件时应接受哪三个SIMD指令集。在编译x86-64结构时,GCC默许使用SSE2指令集。倘若要启用AVX指令集,请使用编写翻译器标识:

-mavx

亟待专心的是,要运营二进制文件的机器必需扶助AVX指令集。您还足以让通过下边包车型大巴选项,让编写翻译器本身决定选择机器中的哪个指令集:

-march=native

若你想要使用 C++11 的特征,比方:lambda 表明式,必须求显明开启了新的C++
Standard:

-std=gnu++0x

4.1. 将协会阵列改为阵列构造。 不要使用缓冲区分配中的多个层级。

升迁代码品质的率先个格局是将协会阵列 (AoS卡塔尔国 改为阵列结构 (SoA卡塔尔国。 (SoA)可扩大单位步长访谈的数据量。
大家不定义大批量全面元组(a、b、c)和根元组的(x1、x2),而是重新安插数据布局,以便将其分配至
5 个巨型阵列:a、b、c、x1 和 x2(参照他事他说加以考察程序 myVectorizedApp.c)。
其它,大家不使用 malloc 分配内部存款和储蓄器,而利用 _mm_malloc 使数据对齐 六十十人边界(见下一章节)。

澳门新葡亰网址下载 8

启用auto-vectorization最好奉行


4.2. 别的属性提高措施:衰亡类型调换、数据对齐

下一步是革除没有必要的类型转变。 举例,函数 sqrt() 将双精度视作输入。
但由于大家将单精度作为本程序的输入,由此编写翻译器须求将单精度转变为双精度。
为了排除无需的数据类型转变,大家得以选拔 sqrtf(),而非 sqrt()
同样,大家不应用整数,而选用单精度。
比如,我们不利用 4,而使用 4.0f。 注意,4.0(未有后缀
f)为双精度浮点数,而 4.0f 为单精度浮点数。

多少对齐有帮忙数据火速地在内部存款和储蓄器之间活动。
对英特尔至强融核微机来说,当数码早先地址坐落于 64
字节边界时,内部存款和储蓄器数据移动可落成最棒状态,就好像英特尔® 至强融核™
协助管理理器同样。 为救助理编辑译器进行矢量化,必要凭借 64人对齐举行内部存款和储蓄器分配,并运用编写翻译提示/指令,在那之中多少可用来告知编写翻译器内部存款和储蓄器访谈已对齐。
最佳凭仗适当对齐的多寡开展矢量化。
本文中所述的矢量化指能够用单指令管理多个数据 (SIMD卡塔尔(قطر‎。

在上述示范中,为对齐堆分配的多寡,我们运用 _mm_malloc() 和 _mm_free() 来分配阵列。
注意,_mm_malloc() 相当于 malloc(),但它将对齐参数(以位为单位)用作第贰个参数,即面向英特尔至强融核微处理器的
64 位。
大家要求在数量前边插入二个子句,告知编写翻译器使用的 assume_aligned(a, 64)代表阵列 a 已对齐。
为报告编译器特定循环中拜望的全数阵列均已对齐,可在循环前边增加子句 #pragma vector aligned

应用数组构造


想要启用自动向量化,编写翻译器不仅仅必要能够深入分析循环迭代,还亟需能够深入分析采访内部存款和储蓄器的格局。嵌套数组的繁缛类型很难或超小概由编写翻译器自动进行矢量化。建议采纳简便的c数组orstd
:: arrays(在C ++
11中引进)来保存数据,并允许编写翻译器以一连的点子访问数据。那也将使您的主次能够利用CPU的种种缓存。

倘令你需求可变数组,建议接受std :: vectorand
获取指向第叁个因素的指针来做客循环中的成分:

std::vectormyData(23);

double * pMyData = &myData.front();

...

pMyData[i] += 5;

数组构造的欧洲经济共同体示例能够在上面包车型地铁“综合代码示例”一节中找到。

4.3. 利用自动矢量化、运转编写翻译器报告,并经过编译器开关禁止使用矢量化

矢量化指利用矢量管理单元 (VPU卡塔尔 同有的时候间运算八个值的编制程序技巧。
自动矢量化指编写翻译器能够辨识循环中的机会并推行相应的矢量化。
你能够丰盛利用Intel编写翻译器的自行矢量化成效,因为机关矢量化暗许协助优化层 –O2 或越来越高层级。

比方说,使用AMD编写翻译器 icc 编译 mySerialApp.c 示例代码时,编译器暗中认可查找循环中的矢量化学工业机械会。
但编写翻译器须求坚守一定的中规中矩(必需知道循环运维、单进单出、直线式代码、嵌套的最内层循环等等),以对那么些循环进行矢计量化验管理。
你能够提供更加多消息,支持编写翻译器对循环实行矢计量化验管理。

为分明代码是或不是曾经落实矢量化,能够透过点名选项 -qopt-report=5 -qopt-report-phase:vec,生成矢量化报告。
之后编写翻译器会生成一份矢量化报告 (.optrpt*State of Qatar。
该报告会告诉您各循环是或不是已到位矢量化,并简短介绍循环矢量化。
注意,矢量化报告选取为 –qopt-report=<n>,在这之中的 n 用于规定细节品级。

界定分支


尽力节制for循环内的分层。
GCC能够将部分if语句翻译成向量化的代码,但超级轻松。那样做时,将评估全部希望的道岔,并废弃未利用分段的值。

4.4. 信任优化层 –O3 举行编译

最近大家可依据优化层 –O3 举行编写翻译。
该优化层可落成最高速度,其优化程度远超过暗许优化层 –O2

依附自动矢量化,编写翻译器将 16个单精度浮点数打包在矢量贮存器中并对该矢量试行运算,并非在每一种迭代循环中壹回拍卖一个因素。

$ icc myVectorizedApp.c –O3 -qopt-report -qopt-report-phase:vec -o myVectorizedApp

编写翻译器生成以下输出文件:二进制文件 myVectorizedApp 和矢量化报告 myVectorizedApp.optrpt
如要运维二进制文件:

$ ./myVectorizedAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....Elapsed time in msec: 30496 (after 10 iterations)

该二进制文件运转时仅使用一个线程,但接受了矢量化。 myVectorizedApp.optrpt 报告应确认是还是不是有所内层循环都已造成矢量化。

进展自查自纠时,还需利用 -no-vec 选项编写翻译该程序:

$ icc myVectorizedApp.c –O3 -qopt-report -qopt-report-phase:vec -o myVectorizedApp-noVEC -no-vecicc: remark #10397: optimization reports are generated in *.optrpt files in the output location

最近运作 myVectorizedApp–noVEC 二进制文件:

$ ./myVectorizedApp-noVECNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....Elapsed time in msec: 180375 (after 10 iterations)

这次 myVectorizedApp.optrpt 告知突显由于禁止使用了机关矢量化功能,因而循环未有到位矢量化,与预期相似。

最近我们得以洞察到四次质量进步。 从原本版本(461,222 纳秒)到 no-vec
版本(180,375 版本)的属性提高珍视得益于选用通用优化技巧。
从未矢量化版本(180,375 皮秒)到矢量化版本(30,496
飞秒)的习性提高着重得益于自动矢量化。

即便完毕了品质提高,依旧只有三个线程实施运算。
扶助七个线程并行运维,可进一层增加代码,进而充足利用多核布局。

用尽全力简化代码


假设在for循环中有复杂的思考,您希望GCC对其打开矢量化,能够考虑采用C ++
11中的lambda表明式,将它们讲解为更简短的代码。在这里间能够找到C
++11新职能的牵线:http://en.wikipedia.org/wiki/Anonymous_function#C.2B.2B

此地是三个lambda函数的例子,用来计量二个double变量的平方。

auto kernel_square =

[] // capture nothing

( double const& a) // take 1 parameter by reference

->double // lambda function returns the square

{

return ( a * a );

};

在循环中调用lambda函数:

for ( unsigned int i = 0; i < size; ++ i)

{

ptr_square[i] = kernel_square( ptr_x[i] ) + kernel_square( ptr_y[i] ) + kernel_square( ptr_z[i] ) ;

}

请小心,此代码将由GCC自动矢量化。对lambda函数的调用不会像经常函数调用那样,产生特别大的开荒,因为代码会被全然内联。

另叁个更宏观的例子是lambda函数中的for循环。那些轮回也会被GCC自动矢量化:

// Defining a compute kernel to encapsulate a specific computation

auto kernel_multiply =

[ &cFixedMultiply ] // capture the constant by reference of the scope of the lambda expression

( DataArray const& a, DataArray const& b, DataArray & res ) // take 3 parameters by reference

->void // lambda function returns void

{

// simple loop vectorized

for( unsigned int i = 0; i < a.size(); ++ i)

{

res[i] = a[i] * b[i];

}

};

5. 启用三十二线程化

不用调用外界函数


调用外界函数,如exp(),log()等等,会招致循环不能够被向量化。因而,您必得决定循环中的数学生运动算是不是丰富,进而将循环分解。那意味着,在率先个循环中,总括调用exp()时,供给采用的参数,并将此结果存款和储蓄在临时数组中。GCC会自动矢量化那个轮回。第二个巡回将轻巧地实施对exp()的调用并储存结果。

设若要调用的函数是由你决定的,请尽量尝试将此函数内定为C ++ 11
lambda表明式。能够在此找到该天性的介绍:http://en.wikipedia.org/wiki/Anonymous_function#C.2B.2B

5.1. 线程层并行化: OpenMP*

为丰富利用AMD至强融核微处理机中多少比非常大的内核(本系统中 陆19个水源),能够通过相互运转 OpenMP 线程来扩张应用。 OpenMP
是面向分享内部存储器的标准 API 和编制程序模型。

行使 OpenMP 线程时,须要满含头文件”omp.h“并延续代码和标志 –qopenmp
在 myParallelApp.c 程序中,可在 for-loop 前拉长以下指令:

#pragma omp parallel for simd

增多在 for-loop 前的编写翻译提醒可告知编写翻译器生成一组线程并将 for-loop
中的工作分为五个数据块。 各样线程遵照 OpenMP
运转时调节实践几个数据块。 SIMD 结构仅呈现应用 SIMD 指令可同一时候施行的高频循环迭代。
它会报告编写翻译器忽视循环中的假定矢量信任性,因而需稳重使用。

在本程序中,线程并行化和矢量化在同叁个循环中进行。
每一种线程从循环的下限开头。 为担保OpenMP(静态调节)得到优越的对齐效果,大家能够界定并行循环的多少,并以串市价势管理其余循环。

澳门新葡亰网址下载 9

此外,总括根的函数将产生

澳门新葡亰网址下载 10

明日你能够编写翻译该程序,并将其三翻五次至 –qopenmp

$ icc myParallelApp.c –O3 -qopt-report=5 -qopt-report-phase:vec,openmp -o myParallelAppl -qopenmp

查看 myParallelApp.optrpt 报告,确认循环是不是已凭借 OpenMP
完毕了矢量化和并行化。

在循环中动用普通的卡尺头流速计


利用普通的整数流量计来营造for循环,并不是std ::
iterator,因为那么些使得GCC难以深入分析内部存款和储蓄器采访和循环的习性,如迭代计数。

for (vector::iterator it = y.begin(); it != y.end(); it++)

{

(*it) += 23;

}

变为:

for (unsigned int i = 0; i < y.size(); ++i)

{

y[i] += 23;

}

5.2. 施用遭遇变量设置线程数量和相近性

OpenMP 履行可同临时间开动多少个线程。
暗许景况下,线程数量设为系统中的最多硬件线程。 本案例上将私下认可运营 272 个
OpenMP 线程。 但是大家还是可以采纳 OMP_NUM_THREADS 情状变量设定 OpenMP
线程的数码。 比方,以下命令可运营 68 个 OpenMP 线程:

$ export OMP_NUM_THREADS=68

使用 KMP_AFFINITY 境遇变量可设置线程相通性(能够将 OpenMP 线程绑定至
CPU)。 为使线程均匀布满于系统,可将变量设置为分散 (scatter卡塔尔:

$ export KMP_AFFINITY=scatter

今昔可应用系统中的全部根本来运行程序,并转移每内核运维的线程数。
以下是测量检验输出,测验对每内核分别运转 1、2、3、4
个线程时的性质实行了对待。

测量检验系统中每内核运营 1 个线程:

$ export KMP_AFFINITY=scatter$ export OMP_NUM_THREADS=68$ ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 1722 (after 10 iterations)

每内核运行 2 个线程:

$ export OMP_NUM_THREADS=136$ ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 136, N = 536870912, N1 = 536869248, num-iters in remainder serial loop = 1664, parallel-pct = 99.999690Starting Compute on 136 threadsElapsed time in msec: 1781 (after 10 iterations)

每内核运转 3 个线程:

$ export OMP_NUM_THREADS=204$ ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 204, N = 536870912, N1 = 536869248, num-iters in remainder serial loop = 1664, parallel-pct = 99.999690Starting Compute on 204 threadsElapsed time in msec: 1878 (after 10 iterations)

每内核运维 4 个线程:

$ export OMP_NUM_THREADS=272$ ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 272, N = 536870912, N1 = 536867072, num-iters in remainder serial loop = 3840, parallel-pct = 99.999285Starting Compute on 272 threadsElapsed time in msec: 1940 (after 10 iterations)

从上述结果可看见,每内核运维 1 个线程并使用一切 六贰拾三个内核时质量达到最棒。

归咎代码示例


下边包车型大巴代码给出了有的有关什么用GCC编写自动矢量化C
++代码的例证。复制并粘贴源代码并使用上边三令五申进行编写翻译就可以:

g++ -Ofast -ftree-vectorizer-verbose=7 -march=native -std=c++11 -o autovect autovect.cpp

请确认保障使用的gcc版本起码是:GCC 4.7

/*

   Compile with ( use at least gcc 4.7 ):
   g++ -Ofast -ftree-vectorizer-verbose=7 -march=native -std=c++11 -o autovect autovect.cpp

*/

#include <math.h>

#include <string>
#include <iostream>
#include <array>
#include <vector>

// Sturcture-Of-Array to hold coordinates
struct Vector3
{
   std::vector<double> x;
   std::vector<double> y;
   std::vector<double> z;

   // final result of the distance calcualtion
   std::vector<double> distance;

   void add( double _x, double _y, double _z )
   {
      x.push_back( _x );
      y.push_back( _y );
      z.push_back( _z );
      distance.push_back( 0.0f );
   }

};


int main()
{

// Fixed Size Arrays

   typedef std::array<double, 10> DataArray;

   DataArray vect_a = { 0,1,2,3,4,5,6,7,8,9 };
   DataArray vect_b = {0.5,1,2,3,4,5,6,7,8,9 };
   DataArray vect_res_plain = { 0,0,0,0,0,0,0,0,0,0};   
   DataArray vect_res_lambda = { 0,0,0,0,0,0,0,0,0,0};

   constexpr double cFixedMultiply = 23.5f;

   // simple loop vectorized
   // -- auto-vectorized --
   for( unsigned int i = 0; i < vect_a.size(); ++ i)
   {
      vect_res_plain[i] = vect_a[i]  + vect_b[i];
   }

   // Defining a compute kernel to encapsulate a specific computation
   auto kernel_multiply = 
      [ &cFixedMultiply ] // capture the constant by reference of the scope of the lambda expression
      ( DataArray const& a, DataArray const& b, DataArray & res ) // take 3 parameters by reference
      ->void // lambda function returns void
   {
      // simple loop vectorized
      // -- auto-vectorized --
      for( unsigned int i = 0; i < a.size(); ++ i)
      {
         res[i] = a[i] * b[i] * cFixedMultiply;
      }
   };

   // call the lambda function
   // this call is autovectorized
   kernel_multiply ( vect_a, vect_b, vect_res_lambda );


   // This kernel will be called multiple times and performs the quadrature
   auto kernel_square = 
      [] // capture nothing
      ( double const& a) // take 1 parameter by reference
      ->double // lambda function returns the square
   {
      return ( a * a );
   };

   // create struct and fill with dummy values
   Vector3 v3;
   for ( unsigned int i = 0; i < 50 ; ++ i)
   {
      v3.add( i * 1.1, i * 2.2,  i* 3.3 );   
   }

   // store the size in a local variable. This is needed to GCG
   // can estimate the loop iterations
   const unsigned int size = v3.x.size();

   // -- auto-vectorized --
   for ( unsigned int i = 0; i < size; ++ i)
   {
      v3.distance[i] = sqrt( kernel_square( v3.x[i] ) + kernel_square( v3.y[i] ) + kernel_square( v3.z[i] )) ;
   }

   // output the result, so GCC won't optimize the calculations away
   std::cout << std::endl << "Computation on std::array" << std::endl;
   for( unsigned int i = 0; i < vect_a.size(); ++ i)
   {
      std::cout << vect_res_plain[i] << std::endl;
      std::cout << vect_res_lambda[i] << std::endl;
   }

   std::cout << std::endl << "Computation on Structure-of-Array with variable sized std::vectors" << std::endl;
   for( unsigned int i = 0; i < v3.x.size(); ++ i)
   {
      std::cout << "sqrt( " << v3.x[i] << "^2 + " << v3.y[i] << "^2 + " << v3.z[i] << "^2 ) = " 
                << v3.distance[i] << std::endl;
   }

   return 0;
}

6. 面向AMD至强融核微处理器优化内核

活动矢量化的骨子里行使


在这里处,您能够找到一部分利用GCC编译器的活动矢量化功用的施用列表:
The VDT mathematical
library

参考:
https://twiki.cern.ch/twiki/bin/view/CMSPublic/WorkBookWritingAutovectorizableCode

6.1. 内部存款和储蓄器带宽优化

系统中有二种内部存款和储蓄器:16 GB 封装内部存款和储蓄器 MCDRAM 和 96 GB 守旧平台 6 通道 DD宝马7系4
RAM(依附选项最高可扩张至 384 GB)。 MCDRAM 带宽度大概为 500 GB/秒,而 DD陆风X84
峰值性能带宽度大约为 90 GB/秒。

有三种适用于 MCDRAM 的布署情势:扁平方式、缓存情势或混合格局。 借使MCDRAM 配置为可寻址内部存款和储蓄器(扁平情势),客户可鲜明地分配 MCDRAM 中的内部存款和储蓄器。
假诺 MCDRAM 配置成缓存形式,整个 MCDRAM 都可用作二级缓存和 DD奥迪Q54
内部存款和储蓄器之间的末级缓存。 借使 MCDRAM 配置成混合格局,部分 MCDRAM
可用作高速缓存,别的部分可用作可寻址内部存储器。
下表列出了这个安插情势的优点和劣点:

内存模式 优点 缺点
扁平模式
  • 用户可控制 MCDRAM 利用高带宽内存
  • 用户需使用 numactl 或修改代码
缓存模式
  • 对用户来说是透明的
  • 扩展缓存级别
  • 加载/保存 DDR 4 中的内存时可能会增加延迟
混合模式
  • 应用可充分利用扁平模式和缓存模式
  • 扁平模式和缓存模式的缺点

至于非一致性内部存款和储蓄器访谈 (NUMA卡塔尔国 结构,依据 MCDRAM
配置格局的例外,Intel至强融核微电脑以叁个或四个节点的样式出现。 假使MCDRAM 配置为缓存情势,AMD至强融核微处理机将以 1 个 NUMA
节点的花样现身。 假诺 MCDRAM
配置为扁平或混合形式,速龙至强融核微处理器将以 2 个 NUMA
节点的款型现身。 注意,集群格局可将英特尔至强融核微处理机进一层细分成 8 个
NUMA 节点;可是本课程暂不涉及集群情势。

使用 numactl 实用程序可突显系统中的 NUMA 节点。
比方,在本系统进行“numactl –H” — 个中 MCDRAM 配置为扁平格局,将显示 2
个 NUMA 节点。 节点 0 包罗 272 个 CPU 和 96 GB DDLX5704 内部存款和储蓄器,节点 1 满含 16
GB MCDRAM。

$ numactl -Havailable: 2 nodes (0-1)node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271node 0 size: 98200 MBnode 0 free: 92888 MBnode 1 cpus:node 1 size: 16384 MBnode 1 free: 15926 MBnode distances:node 0 10: 10 311: 31 10

在部分 NUMA 模式下,可使用”numactl” 工具分配内部存款和储蓄器。 本示例中,节点 0
富含全数 CPU 和平台内部存款和储蓄器 DDWrangler4,节点 1 满含封装内部存款和储蓄器 MCDRAM。
可使用开关 –m 或 –-membind 倒逼程序将内部存款和储蓄器分配至有些 NUMA 节点。

要是反逼应用分配 DD哈弗 内部存款和储蓄器(节点 0),可运营以下命令:
$ numactl -m 0 ./myParallelApp

这一定于:
$ ./myParallelApp

近期应用 68 个线程来运转应用:

$ export KMP_AFFINITY=scatter$ export OMP_NUM_THREADS=68

$ numactl -m 0 ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 1730 (after 10 iterations)

如出示 NUMA 节点的任何视图,可运维命令“lstopo”。 该命令不独有展现 NUMA
节点,还显得与那几个节点相关的相当高速缓存和二级高速缓存。

6.2. 剖判内部存款和储蓄器使用率

使用是或不是受带宽节制? 使用英特尔 VTune Amplifier 解析内部存款和储蓄器访谈。 DDLacrosse4 DRAM
峰值性能带宽度约为 90 GB/秒,MCDRAM 内部存款和储蓄器峰值质量约为 500 GB/秒。

在系统上安装Intel VTune Amplifier,然后运营以下英特尔 VTune Amplifier
命令,以搜集应用分配 DDTucson 内部存款和储蓄器时的内部存款和储蓄器访谈新闻:

$ export KMP_AFFINITY=scatter; export OMP_NUM_THREADS=68; amplxe-cl -collect memory-access -- numactl -m 0 ./myParallelApp

因而翻看“带宽利用耿直方图”字段,能够了然应用的带宽利用率。 该直方图显示DD本田CR-V 带宽利用率较高。

澳门新葡亰网址下载 11

经过查看内存访问解析我们发掘,DDWrangler4 最高带宽为 96 GB/秒,这几乎是 DDRubicon4
的峰值质量带宽。 该结果表明应用受带宽限定。

澳门新葡亰网址下载 12

因而查看应用的内部存款和储蓄器分配,我们发掘分配了饱含 512 M 成分(即 512 * 1024 *
1024 个因素)的 5 个巨型阵列。 每一种成分皆以单精度浮点(4
字节)数;由此种种阵列的大大小小约为 4*512 M 或 2 GB。 总内部存款和储蓄器分配为 2 GB *
5 = 10 GB。 这种内部存储器大小特别符合 MCDRAM(16 GB 容积),因此分配 MCDRAM
中的内存(扁平情势)将有益于该选用。

分红 MCDRAM 中的内部存款和储蓄器(节点 1)时,需将参数–m 1
传递至命令 numactl,如下所示:

$ numactl -m 1 ./myParallelAppNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 498 (after 10 iterations)

鲜明,应用分配 MCDRAM 中的内存时,质量获得了显眼提高。

为开展对照,大家运维英特尔 VTune Amplifier 命令,搜集应用分配 MCDRAM
内存时的内部存款和储蓄器访谈音讯:

$ export KMP_AFFINITY=scatter; export OMP_NUM_THREADS=68; amplxe-cl -collect memory-access -- numactl -m 1 ./myParallelApp

该直方图展现 DDSportage 带宽利用率超级低,而 MCDRAM 的利用率较高:

澳门新葡亰网址下载 13

澳门新葡亰网址下载 14

透过查看内部存款和储蓄器访谈深入分析大家开采,DDQX564 峰值带宽为 2.3 GB/秒,而 MCDRAM
峰值带宽达到了 437 GB/秒。

澳门新葡亰网址下载 15

6.3. 用到编写翻译器手柄 –xMIC-AVX512 实行编写翻译

英特尔至强融核微型机援助 x87、英特尔® SIMD 流指令扩大(AMD®
SSE)、英特尔® SSE2、AMD® SSE3、SIMD 流指令增添 3 补充版、Intel®
SSE4.1、Intel® SSE4.2、AMD® 高等矢量扩张指令集(英特尔®
AVX)、AMD® 高端矢量扩充指令集 2(Intel® AVX2)和AMD®
高端矢量扩展指令集 512 (AMD® AVX-512)指令集布局 (ISA卡塔尔国,
但不帮助英特尔® 交易同步扩充。

英特尔 AVX-512 在英特尔至强融核微机中进行。
英特尔至强融核微电脑匡助以下组: Intel AVX-512F、英特尔AVX-512CD、AMD AVX-512ELacrosse 和英特尔 AVX-FP。 Intel AVX-512F(英特尔AVX-512 根基指令)包含适用于 512 位少量存放器的AMD AVX 和Intel AVX2
SIMD 流指令;Intel AVX-512CD(Intel AVX-512
冲突检查实验)有协理飞速检验冲突,以支撑越来越多循环完毕矢量化;英特尔AVX-512ETucson(AMD AVX-512 指数和尾数指令)为以 2
为底的指数函数、尾数和平方根倒数提供指令。 英特尔 AVX-512PF(英特尔AVX-512 预取指令)有援助收缩内存操作延迟。

为丰裕利用速龙 AVX-512,可依据编写翻译手柄 –xMIC-AVX512 编写翻译程序
$ icc myParallelApp.c -o myParallelApp-AVX512 -qopenmp -O3 -xMIC-AVX512

$ export KMP_AFFINITY=scatter$ export OMP_NUM_THREADS=68

$ numactl -m 1 ./myParallelApp-AVX512No. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 316 (after 10 iterations)

介意,今后得以运作以下命令,生成名称叫 myParallelApp.s 的汇编文件:

$ icc -O3 myParallelApp.c -qopenmp -xMIC-AVX512 -S -fsource-asm

因此检查汇编文件,能够确认是或不是生成了英特尔 AVX512 ISA。

6.4. 使用 –no-prec-div -fp-model fast=2 优化标志。

即便不需求高精度,我们能够动用 -fp-model fast=2举行编写翻译,大胆应用浮点模型,进而越发优化浮点数(但不太安全)。
编译器推行更迅捷、精度相当低的平方根和除法运算。 比如:

$ icc myParallelApp.c -o myParallelApp-AVX512-FAST -qopenmp -O3 -xMIC-AVX512 -no-prec-div -no-prec-sqrt -fp-model fast=2$ export OMP_NUM_THREADS=68

$ numactl -m 1 ./myParallelApp-AVX512-FASTNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 310 (after 10 iterations)

6.5. 将 MCDRAM 配置成缓存

在 BIOS 设置中,将 MCDRAM
配置成缓存同样重视启系统。 numactl 实用程序可确认是否唯有叁个 NUMA
节点,因为 MCDRAM 配置成缓存后,对该实用程序来讲是晶莹剔透的:

$ numactl -Havailable: 1 nodes (0)node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271node 0 size: 98200 MBnode 0 free: 94409 MBnode distances:node 00: 10

重新编写翻译该程序:

$ icc myParalledApp.c -o myParalledApp -qopenmp -O3 -xMIC-AVX512 -no-prec-div -no-prec-sqrt -fp-model fast=2

并运行该程序:

$ export OMP_NUM_THREADS=68$ ./myParalledApp-AVX512-FASTNo. of Elements : 512MRepetitions = 10Start allocating buffers and initializing ....thread num=0Initializingnumthreads = 68, N = 536870912, N1 = 536870336, num-iters in remainder serial loop = 576, parallel-pct = 99.999893Starting Compute on 68 threadsElapsed time in msec: 325 (after 10 iterations)

侦察该应用元帅 MCDRAM 用作高速缓存是不是没有别的优势。

7. 总计和结论

本课程介绍了以下主旨:

  • 内部存款和储蓄器对齐
  • 矢量化
  • 转移编写翻译器报告以救助代码剖判
  • 利用命令行实用程序 cpuinfo、lscpu、numactl、lstopo
  • 运用 OpenMP 增多线程层并行化
  • 安装意况变量
  • 采纳Intel VTune Amplifier 深入分析带宽利用率
  • 使用 numactl 分配 MCDRAM 内存
  • 利用英特尔 AVX512 标识实行编写翻译,以晋级品质

下表突显了从标准代码完毕每二个优化步骤后所达成的个性升高:基于数据对齐的通用优化、矢量化、加多线程层并行化、以扁平方式分配
MCDRAM 内部存款和储蓄器、依附英特尔 AVX512 实行编写翻译、依据无精度标志举办编写翻译,以致将
MCDRAM 用作高速缓存。

澳门新葡亰网址下载 16

通过使用一切可用内核、英特尔 AVX-512 矢量化和 MCDRAM
带宽,我们能够小幅度缩水实践时间。

参谋资料:

  • 面向英特尔® 至强融核™ 微型机以致英特尔® AVX-512 ISA
    的编写翻译
  • Knights Landing 上的
    MCDRAM(高带宽内部存款和储蓄器)简要介绍
  • 英特尔® C++
    编写翻译器矢量化指南
  • 在 Stream Triad 上优化 奈特s Landing
    的内存带宽
  • AMD® 至强融核™
    协微电脑上的流管理
  • 数据对齐有利于达成矢量化
  • 为在英特尔® 至强™ 微电脑上贯彻最棒质量的内部存储器处理:
    对齐和预取
  • Intel® MIC
    架构高等优化
  • 运用 OpenMP 4.0 在程序中启用
    SIMD
  • Intel® 至强™ 微处理器 — 内部存款和储蓄器情势和集群形式:
    配置和用例

有关笔者

Loc Q Nguyen 具有奥克兰高校 MBA
学位、麦Gill大学电子工程标准大学子学位以致河内理工科高校电子工程职业余大学学生学位。
前段时间在英特尔集团软件及劳动工作部担当软件程序员。
商讨领域包含Computer网络、并行计算和Computer图形。

属性测验中应用的软件和劳作负荷或者仅在AMD® 微处理机上进行了质量优化。
诸如SYSmark和MobileMark等测验均系基于特定Computer种类、硬件、软件、操作系统及效果,
上述任何因素的退换都有希望引致测量检验结果的更换。
请参照他事他说加以侦察别的新闻及品质测量检验(包蕴结合别的付加物选取时的周转品质)以对目的成品进行周到评估。
如欲掌握更加多音信,请访谈.

英特尔示例源代码许可合同

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图