基于SSE指令集的程序设计简介

** 基于SSE指令集的程序设计简介 **

作者:Alex Farber
出处: http://www.codeproject.com/cpp/sseintro.asp

SSE技术简介


Intel公司的单指令多数据流式扩展(SSE,Streaming SIMD Extensions)技术能够有效增强CPU浮点运算的能力。Visual Studio .NET 2003提供了对SSE指令集的编程支持,从而允许用户在C++代码中不用编写汇编代码就可直接使用SSE指令的功能。MSDN中有关SSE技术的主题[1]有可能会使不熟悉使用SSE汇编指令编程的初学者感到困惑,但是在阅读MSDN有关文档的同时,参考一下Intel软件说明书(Intel Software manuals)[2]会使你更清楚地理解使用SSE指令编程的要点。

SIMD(single-instruction, multiple-data)是一种使用单道指令处理多道数据流的CPU执行模式,即在一个CPU指令执行周期内用一道指令完成处理多个数据的操作。 考虑一下下面这个任务:计算一个很长的浮点型数组中每一个元素的平方根。实现这个任务的算法可以这样写:

for each f in array //对数组中的每一个元素
f = sqrt(f) //计算它的平方根

为了了解实现的细节,我们把上面的代码这样写:

for each f in array
{
把f从内存加载到浮点寄存器
计算平方根
再把计算结果从寄存器中取出放入内存
}

具有Intel SSE指令集支持的处理器有8个128位的寄存器,每一个寄存器可以存放4个(32位)单精度的浮点数。SSE同时提供了一个指令集,其中的指令可以允许把浮点数加载到这些128位的寄存器之中,这些数就可以在这些寄存器中进行算术逻辑运算,然后把结果放回内存。采用SSE技术后,算法可以写成下面的样子:

for each 4 members in array //对数组中的每4个元素
{
把数组中的这4个数加载到一个128位的SSE寄存器中
在一个CPU指令执行周期中完成计算这4个数的平方根的操作
把所得的4个结果取出写入内存
}

C++编程人员在使用SSE指令函数编程时不必关心这些128位的寄存器,你可以使用128位的数据类型“__m128”和一系列C++函数来实现这些算术和逻辑操作,而决定程序使用哪个SSE寄存器以及代码优化是C++编译器的任务。当需要对很长的浮点数数组中的元素进行处理的时候,SSE技术确实是一种很高效的方法。

SSE程序设计详细介绍

包含的头文件:

所有的SSE指令函数和__m128数据类型都在xmmintrin.h文件中定义:
#include

  1<xmmintrin.h>   
  2因为程序中用到的SSE处理器指令是由编译器决定,所以它并没有相关的.lib库文件。 
  3
  4**数据分组(Data Alignment)**
  5
  6由SSE指令处理的每一个浮点数数组必须把其中需要处理的数每16个字节(128位二进制)分为一组。一个静态数组(static array)可由 **__declspec(align(16))** 关键字声明: 
  7
  8__declspec(align(16)) float m_fArray[ARRAY_SIZE]; 
  9
 10动态数组(dynamic array)可由 **_aligned_malloc** 函数为其分配空间:   
 11m_fArray = (float*) _aligned_malloc(ARRAY_SIZE * sizeof(float), 16); 
 12
 13由_aligned_malloc函数分配空间的动态数组可以由 **_aligned_free** 函数释放其占用的空间:   
 14_aligned_free(m_fArray); 
 15
 16**__m128 数据类型**
 17
 18该数据类型的变量可用做SSE指令的操作数,它们不能被用户指令直接存取。_m128类型的变量被自动分配为16个字节的字长。 
 19
 20**CPU对SSE指令集的支持**
 21
 22如果你的CPU能够具有了SSE指令集,你就可以使用Visual Studio .NET 2003提供的对SSE指令集支持的C++函数库了,你可以查看MSDN中的一个Visual C++ CPUID的例子[4],它可以帮你检测你的CPU是否支持SSE、MMX指令集或其它的CPU功能。 
 23
 24  
 25**编程实例**   
 26以下讲解了SSE技术在Visual Studio .NET 2003下的应用实例,你可以在  http://www.codeproject.com/cpp/sseintro/SSE_src.zip  下载示例程序压缩包。该压缩包中含有两个项目,这两个项目是基于微软基本类库(MFC)建立的Visual C++.NET项目,你也可以按照下面的讲解建立这两个项目。 
 27
 28** SSETest 示例项目  **
 29
 30SSETest项目是一个基于对话框的应用程序,它用到了三个浮点数组参与运算: 
 31
 32fResult[i] = sqrt( fSource1[i]*fSource1[i] + fSource2[i]*fSource2[i] ) + 0.5 
 33
 34其中i = 0, 1, 2 ... ARRAY_SIZE-1 
 35
 36其中ARRAY_SIZE被定义为30000。数据源数组(Source数组)通过使用sin和cos函数给它赋值,我们用Kris Jearakul开发的瀑布状图表控件(Waterfall chart control)[3] 来显示参与计算的源数组和结果数组。计算所需的时间(以毫秒ms为单位)在对话框中显示出来。我们使用三种不同的途径来完成计算: 
 37
 38纯C++代码;   
 39使用SSE指令函数的C++代码;   
 40包含SSE汇编指令的代码。 
 41
 42  
 43**纯C++代码:**
 44
 45void CSSETestDlg::ComputeArrayCPlusPlus(   
 46float* pArray1, // [输入] 源数组1   
 47float* pArray2, // [输入] 源数组2   
 48float* pResult, // [输出] 用来存放结果的数组   
 49int nSize) // [输入] 数组的大小   
 50{ 
 51
 52int i; 
 53
 54float* pSource1 = pArray1;   
 55float* pSource2 = pArray2;   
 56float* pDest = pResult; 
 57
 58for ( i = 0; i &lt; nSize; i++ )   
 59{   
 60*pDest = (float)sqrt((*pSource1) * (*pSource1) + (*pSource2)   
 61* (*pSource2)) + 0.5f; 
 62
 63pSource1++;   
 64pSource2++;   
 65pDest++;   
 66}   
 67} 
 68
 69  
 70下面我们用具有SSE特性的C++代码重写上面这个函数。为了查询使用SSE指令C++函数的方法,我参考了Intel软件说明书(Intel Software manuals)中有关SSE汇编指令的说明,首先我是在第一卷的第九章找到的相关SSE指令,然后在第二卷找到了这些SSE指令的详细说明,这些说明有一部分涉及了与其特性相关的C++函数。然后我通过这些SSE指令对应的C++函数查找了MSDN中与其相关的说明。搜索的结果见下表: 
 71
 72**实现的功能** |  **对应的SSE汇编指令** |  **Visual C++.NET中的SSE函数**  
 73---|---|---  
 74将4个32位浮点数放进一个128位的存储单元。  |  movss 和 shufps  |  _mm_set_ps1   
 75将4对32位浮点数同时进行相乘操作。这4对32位浮点数来自两个128位的存储单元,再把计算结果(乘积)赋给一个128位的存储单元。  |  mulps  |  _mm_mul_ps   
 76将4对32位浮点数同时进行相加操作。这4对32位浮点数来自两个128位的存储单元,再把计算结果(相加之和)赋给一个128位的存储单元。  |  addps  |  _mm_add_ps   
 77对一个128位存储单元中的4个32位浮点数同时进行求平方根操作。  |  sqrtps  | 
 78
 79_mm_sqrt_ps   
 80  
 81**使用Visual C++.NET的 SSE指令函数的代码:**
 82
 83void CSSETestDlg::ComputeArrayCPlusPlusSSE(   
 84float* pArray1, // [输入] 源数组1   
 85float* pArray2, // [输入] 源数组2   
 86float* pResult, // [输出] 用来存放结果的数组   
 87int nSize) // [输入] 数组的大小   
 88{   
 89int nLoop = nSize/ 4; 
 90
 91__m128 m1, m2, m3, m4; 
 92
 93__m128* pSrc1 = (__m128*) pArray1;   
 94__m128* pSrc2 = (__m128*) pArray2;   
 95__m128* pDest = (__m128*) pResult; 
 96
 97  
 98__m128 m0_5 = _mm_set_ps1(0.5f); // m0_5[0, 1, 2, 3] = 0.5 
 99
100for ( int i = 0; i &lt; nLoop; i++ )   
101{   
102m1 = _mm_mul_ps(*pSrc1, *pSrc1); // m1 = *pSrc1 * *pSrc1   
103m2 = _mm_mul_ps(*pSrc2, *pSrc2); // m2 = *pSrc2 * *pSrc2   
104m3 = _mm_add_ps(m1, m2); // m3 = m1 + m2   
105m4 = _mm_sqrt_ps(m3); // m4 = sqrt(m3)   
106*pDest = _mm_add_ps(m4, m0_5); // *pDest = m4 + 0.5   
107  
108pSrc1++;   
109pSrc2++;   
110pDest++;   
111}   
112} 
113
114**使用SSE汇编指令实现的C++函数代码:**
115
116void CSSETestDlg::ComputeArrayAssemblySSE(   
117float* pArray1, // [输入] 源数组1   
118float* pArray2, // [输入] 源数组2   
119float* pResult, // [输出] 用来存放结果的数组   
120int nSize) // [输入] 数组的大小   
121{   
122int nLoop = nSize/4;   
123float f = 0.5f; 
124
125_asm   
126{   
127movss xmm2, f // xmm2[0] = 0.5   
128shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0] 
129
130mov esi, pArray1 // 输入的源数组1的地址送往esi   
131mov edx, pArray2 // 输入的源数组2的地址送往edx 
132
133mov edi, pResult // 输出结果数组的地址保存在edi   
134mov ecx, nLoop //循环次数送往ecx 
135
136start_loop:   
137movaps xmm0, [esi] // xmm0 = [esi]   
138mulps xmm0, xmm0 // xmm0 = xmm0 * xmm0 
139
140movaps xmm1, [edx] // xmm1 = [edx]   
141mulps xmm1, xmm1 // xmm1 = xmm1 * xmm1 
142
143addps xmm0, xmm1 // xmm0 = xmm0 + xmm1   
144sqrtps xmm0, xmm0 // xmm0 = sqrt(xmm0) 
145
146addps xmm0, xmm2 // xmm0 = xmm1 + xmm2 
147
148movaps [edi], xmm0 // [edi] = xmm0 
149
150add esi, 16 // esi += 16   
151add edx, 16 // edx += 16   
152add edi, 16 // edi += 16 
153
154dec ecx // ecx--   
155jnz start_loop //如果不为0则转向start_loop   
156}   
157} 
158
159最后,在我的计算机上运行计算测试的结果: 
160
161纯C++代码计算所用的时间是26 毫秒   
162使用SSE的C++ 函数计算所用的时间是 9 毫秒   
163包含SSE汇编指令的C++代码计算所用的时间是 9 毫秒 
164
165以上的时间结果是在Release优化编译后执行程序得出的。 
166
167** SSESample 示例项目  **
168
169** **   
170SSESample项目是一个基于对话框的应用程序,其中它用下面的浮点数数组进行计算: 
171
172fResult[i] = sqrt(fSource[i]*2.8) 
173
174其中i = 0, 1, 2 ... ARRAY_SIZE-1 
175
176这个程序同时计算了数组中的最大值和最小值。ARRAY_SIZE被定义为100000,数组中的计算结果在列表框中显示出来。其中在我的机子上用下面三种方法计算所需的时间是:   
177纯C++代码计算 6 毫秒   
178使用SSE的C++ 函数计算 3 毫秒   
179使用SSE汇编指令计算 2 毫秒 
180
181大家看到,使用SSE汇编指令计算的结果会好一些,因为使用了效率增强了的SSX寄存器组。但是在通常情况下,使用SSE的C++ 函数计算会比汇编代码计算的效率更高一些,因为C++编译器的优化后的代码有很高的运算效率,若要使汇编代码比优化后的代码运算效率更高,这通常是很难做到的。 
182
183**纯C++代码:**
184
185// 输入: m_fInitialArray   
186// 输出: m_fResultArray, m_fMin, m_fMax   
187void CSSESampleDlg::OnBnClickedButtonCplusplus()   
188{   
189m_fMin = FLT_MAX;   
190m_fMax = FLT_MIN; 
191
192int i; 
193
194for ( i = 0; i &lt; ARRAY_SIZE; i++ )   
195{   
196m_fResultArray[i] = sqrt(m_fInitialArray[i] * 2.8f); 
197
198if ( m_fResultArray[i] &lt; m_fMin )   
199m_fMin = m_fResultArray[i]; 
200
201if ( m_fResultArray[i] &gt; m_fMax )   
202m_fMax = m_fResultArray[i];   
203}   
204} 
205
206**使用Visual C++.NET的 SSE指令函数的代码:**
207
208  
209// 输入: m_fInitialArray   
210// 输出: m_fResultArray, m_fMin, m_fMax   
211void CSSESampleDlg::OnBnClickedButtonSseC()   
212{   
213__m128 coeff = _mm_set_ps1(2.8f); // coeff[0, 1, 2, 3] = 2.8   
214__m128 tmp; 
215
216__m128 min128 = _mm_set_ps1(FLT_MAX); // min128[0, 1, 2, 3] = FLT_MAX   
217__m128 max128 = _mm_set_ps1(FLT_MIN); // max128[0, 1, 2, 3] = FLT_MIN 
218
219__m128* pSource = (__m128*) m_fInitialArray;   
220__m128* pDest = (__m128*) m_fResultArray; 
221
222for ( int i = 0; i &lt; ARRAY_SIZE/4; i++ )   
223{   
224tmp = _mm_mul_ps(*pSource, coeff); // tmp = *pSource * coeff   
225*pDest = _mm_sqrt_ps(tmp); // *pDest = sqrt(tmp) 
226
227min128 = _mm_min_ps(*pDest, min128);   
228max128 = _mm_max_ps(*pDest, max128); 
229
230pSource++;   
231pDest++;   
232} 
233
234// 计算max128的最大值和min128的最小值   
235union u   
236{   
237__m128 m;   
238float f[4];   
239} x; 
240
241x.m = min128;   
242m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3]))); 
243
244x.m = max128;   
245m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3])));   
246} 
247
248****
249
250**使用SSE汇编指令的C++函数代码:**
251
252****   
253// 输入: m_fInitialArray   
254// 输出: m_fResultArray, m_fMin, m_fMax   
255void CSSESampleDlg::OnBnClickedButtonSseAssembly()   
256{   
257  
258float* pIn = m_fInitialArray;   
259float* pOut = m_fResultArray; 
260
261float f = 2.8f;   
262float flt_min = FLT_MIN;   
263float flt_max = FLT_MAX; 
264
265__m128 min128;   
266__m128 max128; 
267
268// 使用以下的附加寄存器:xmm2、xmm3、xmm4:   
269// xmm2 – 相乘系数   
270// xmm3 – 最小值   
271// xmm4 – 最大值 
272
273_asm   
274{   
275movss xmm2, f // xmm2[0] = 2.8   
276shufps xmm2, xmm2, 0 // xmm2[1, 2, 3] = xmm2[0] 
277
278movss xmm3, flt_max // xmm3 = FLT_MAX   
279shufps xmm3, xmm3, 0 // xmm3[1, 2, 3] = xmm3[0] 
280
281movss xmm4, flt_min // xmm4 = FLT_MIN   
282shufps xmm4, xmm4, 0 // xmm3[1, 2, 3] = xmm3[0] 
283
284mov esi, pIn // 输入数组的地址送往esi   
285mov edi, pOut // 输出数组的地址送往edi   
286mov ecx, ARRAY_SIZE/4 // 循环计数器初始化 
287
288start_loop:   
289movaps xmm1, [esi] // xmm1 = [esi]   
290mulps xmm1, xmm2 // xmm1 = xmm1 * xmm2   
291sqrtps xmm1, xmm1 // xmm1 = sqrt(xmm1)   
292movaps [edi], xmm1 // [edi] = xmm1 
293
294minps xmm3, xmm1   
295maxps xmm4, xmm1 
296
297add esi, 16   
298add edi, 16 
299
300dec ecx   
301jnz start_loop 
302
303  
304movaps min128, xmm3   
305movaps max128, xmm4   
306} 
307
308union u   
309{   
310__m128 m;   
311float f[4];   
312} x; 
313
314x.m = min128;   
315m_fMin = min(x.f[0], min(x.f[1], min(x.f[2], x.f[3]))); 
316
317x.m = max128;   
318m_fMax = max(x.f[0], max(x.f[1], max(x.f[2], x.f[3]))); 
319
320} 
321
322  
323**参考文档:**
324
325[1]MSDN, SSE技术主题:   
326http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/vcrefstreamingsimdextensions.asp 
327
328[2]Intel软件说明书(Intel Software manuals):   
329http://developer.intel.com/design/archives/processors/mmx/index.htm 
330
331[3] Kris Jearakul的瀑布状图表控件: http://www.codeguru.com/controls/Waterfall.shtml 
332
333[4] Microsoft Visual C++ CPUID示例:   
334http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vcsample/html/vcsamcpuiddeterminecpucapabilities.asp 
335
336[5] Matt Pietrek在Microsoft Systems Journal 1998年2月刊上的评论文章:   
337http://www.microsoft.com/msj/0298/hood0298.aspx 。</xmmintrin.h>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus