sse 入門

C語言:內存字節對齊詳解

分類:並行開發技術--SSE2012-03-28 14:436人閱讀評論(0)收藏 舉報

一、什麼是對齊,以及爲什麼要對齊:

1. 現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經常在特定的內存地址訪問,這就需要各類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

2.
對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺的要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設爲 32位)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能得到該int數據。顯然在讀取效率上下降很多。這也是空間和時間的博弈。

二、對齊的實現

通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標平臺的對齊策略。當然,我們也可以通知給編譯器傳遞預編譯指令而改變對指定數據的對齊方法。
但是,正因爲我們一般不需要關心這個問題,所以因爲編輯器對數據存放做了對齊,而我們不瞭解的話,常常會對一些問題感到迷惑。最常見的就是struct數據結構的sizeof結果,出乎意料。爲此,我們需要對對齊算法所瞭解。
對齊的算法:
由於各個平臺和編譯器的不同,現以本人使用的gcc version 3.2.2編譯器(32x86平臺)爲例子,來討論編譯器對struct數據結構中的各成員如何進行對齊的。
設結構體如下定義:
struct A {
    int a;
    char b;
    short c;
};
結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個。所以A用到的空間應該是7字節。但是因爲編譯器要對數據成員在空間上進行對齊。
所以使用sizeof(strcutA)值爲8
現在把該結構體調整成員變量的順序。
struct B {
    char b;
    int a;
    short c;
};
這時候同樣是總共7個字節的變量,但是sizeof(structB)的值卻是12
下面我們使用預編譯指令#pragmapack (value)來告訴編譯器,使用我們指定的對齊值來取代缺省的。
#progma pack (2) /*
指定按2字節對齊*/
struct C {
    char b;
    int a;
    short c;
};
#progma pack () /*
取消指定對齊,恢復缺省對齊
*/
sizeof(struct C)
值是8


修改對齊值爲1
#progma pack (1) /*
指定按1字節對齊*/
struct D {
    char b;
    int a;
    short c;
};
#progma pack () /*
取消指定對齊,恢復缺省對齊
*/
sizeof(struct D)
值爲7


char型數據,其自身對齊值爲1,對於short型爲2,對於int,float,double類型,其自身對齊值爲4,單位字節。
這裏面有四個概念值:
1)數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。

2)
指定對齊值:#pragmapack (value)時的指定對齊值value

3)
結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。

4)
數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小的那個值。

       有了這些值,我們就可以很方便的來討論具體數據結構的成員和其自身的對齊方式。有效對齊值N是最終用來決定數據存放地址方式的值,最重要。有效對齊N,就是表示對齊在N,也就是說該數據的"存放起始地址%N=0".而數據結構中的數據變量都是按定義的先後順序來排放的。第一個數據變量的起始地址就是數據結構的起始地址。結構體的成員變量要對齊排放,結構體本身也要根據自身的有效對齊值圓整(就是結構體成員變量佔用總長度需要是對結構體有效對齊值的整數倍,結合下面例子理解)這樣就不難理解上面的幾個例子的值了。
例子分析:
分析例子B
struct B {
    char b;
    int a;
    short c;
};
假設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認爲4。第一個成員變量b的自身對齊值是1,比指定或者默認指定對齊值4小,所以其有效對齊值爲1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值爲4,所以有效對齊值也爲 4,所以只能存放在起始地址爲0x00040x0007這四個連續的字節空間中,複覈0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊值爲2,所以有效對齊值也是2,可以存放在0x00080x0009這兩個字節空間中,符合0x0008%2=0。所以從0x00000x0009放的都是B內容。再看數據結構B的自身對齊值爲其變量中最大對齊值(這裏是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求, 0x00090x0000=10字節,(102)%40。所以0x0000A0x000B也爲結構體B所佔用。故B0x00000x000B共有12個字節,sizeof(structB)=12;

同理,分析上面例子C

#pragma pack (2) /*
指定按2字節對齊*/
struct C {
    char b;
    int a;
    short c;
};
#pragma pack () /*
取消指定對齊,恢復缺省對齊
*/
第一個變量b的自身對齊值爲1,指定對齊值爲2,所以,其有效對齊值爲1,假設C0x0000開始,那麼b存放在0x0000,符合0x0000%1=0;第二個變量,自身對齊值爲4,指定對齊值爲2,所以有效對齊值爲2,所以順序存放在0x00020x00030x00040x0005四個連續字節中,符合0x0002%2=0。第三個變量c的自身對齊值爲2,所以有效對齊值爲2,順序存放

0x00060x0007中,符合0x0006%2=0。所以從0x00000x00007共八字節存放的是C的變量。又C的自身對齊值爲4,所以 C的有效對齊值爲2。又8%2=0,C只佔用0x00000x0007的八個字節。所以sizeof(structC)=8.

了以上的解釋,相信你對C語言的字節對齊概念應該有了清楚的認識了吧。在網絡程序中,掌握這個概念可是很重要的喔,在不同平臺之間(比如在WindowsLinux之間)傳遞2進制流(比如結構體),那麼在這兩個平臺間必須要定義相同的對齊方式,不然莫名其妙的出了一些錯,可是很難排查的哦^_^

分享到:

· 上一篇:__declspec關鍵字詳細用法

· 下一篇:SSE指令算法及應用----入門篇

SSE指令算法及應用----入門篇

分類:並行開發技術--SSE2012-03-28 16:2912人閱讀評論(0)收藏 舉報

SSE是英特爾提出的即MMX之後新一代(當然是幾年前了)CPU指令集,最早應用在PIII系列CPU上。現在已經得到了Intel PIII、P4、Celeon、Xeon、AMD Athlon、duron等系列CPU的支持。而更新的SSE2指令集僅得到了P4系列CPU的支持,這也是爲什麼這篇文章是講SSE而不是SSE2的原因之一。另一個原因就是SSE和SSE2的指令系統是非常相似的,SSE2比SSE多的僅是少量的額外浮點處理功能、64位浮點數運算支持和64位整數運算支持。

  SSE爲什麼會比傳統的浮點運算更快呢?因爲它使用了128位的存儲單元,這對於32位的浮點數來講,是可以存下4個的,也就是說,SSE中的所有計算都是一次性針對4個浮點數來完成的,這種批處理當然就會帶來效率的提升。我們再來回顧一下SSE的全稱:Stream SIMD Extentions(流SIMD擴展)。SIMD就是singleinstruction multiple data,連起來就是“數據流單指令多數據擴展”,從名字我們就可以更好的理解SSE是如何工作的了。

 雖然SSE從理論上來講要比傳統的浮點運算會快,但是他所受的限制也很多,首先,雖然他執行一次相當於四次,會比傳統的浮點運算執行4次的速度要快,但是他執行一次的速度卻並沒有想象中的那麼快,所以要體現SSE的速度,必須有Stream做前提,就是大量的流數據,這樣才能發揮SIMD的強大作用。其次,SSE支持的數據類型是4個32位(共計128位)浮點數集合,就是C、C++語言中的float[4],並且必須是以16字節邊界對齊的(稍後會以代碼來進行闡釋,關於邊界對齊的概念,讀者可以參考論壇上的其它文章,都會有很詳細的解答,我這裏就恕不贅述了)。因此這也給輸入和輸出帶來了不少的麻煩,實際上主要影響SSE發揮性能的就是不停的對數據進行復制以適用應它的數據格式。

 我是一個C++程序員,對彙編並不很熟,但我又想用SSE來優化我的程序,我該怎麼做呢?幸好VC++.net爲我們提供了很方便的指令C函數級的封裝和C格式數據類型,我們只需像平時寫C++代碼一樣定義變量、調用函數就可以很好的應用SSE指令了。

 當然了,我們需要包含一個頭文件,這裏麪包括了我們需要的數據類型和函數的聲明:

 

#include <xmmintrin.h>

  SSE運算的標準數據類型只有一個,就是:

__m128,它是這樣定義的:

 

typedef struct __declspec(intrin_type)  __declspec(align(16)) __m128 {

   float m128_f32[4];

} __m128;

 簡化一下,就是:

 

struct __m128

{

   float m128_f32[4];

};

 比如要定義一個__m128變量,併爲它賦四個float整數,可以這樣寫:

 

 

__m128 S1 = { 1.0f, 2.0f,  3,0f, 4,0f };

 

 要改變其中第2個(基數爲0)元素時可以這樣寫:

 

S1.m128_f32[2] = 6.0f;

 令外我們還會用到幾個賦值的指令,它可以讓我們更方便的使用這個數據結構:

 

S1  = _mm_set_ps1( 2.0f );

 它會讓S1.m128_f32中的四個元素全部賦予2.0f,這樣會比你一個一個賦值要快的多。

 

S1 = _mm_setzero_ps();

 這會讓S1中的所有4個浮點數都置零。

 還有一些其它的賦值指令,但執行起來還沒有自己逐個賦值來的快,只做爲一些特殊用途,如果你想了解更多的信息,可以參考MSDN -> VisualC++參考 -> C/C++Language-> C++Language Reference -> Compiler Intrinsics -> MMX, SSE, and SSE2Intrinsics -> Stream SIMD Extensions(SSE)章節。

 一般來講,所有SSE指令函數都有3個部分組成,中間用下劃線隔開:

 

_mm_set_ps1

 mm表示多媒體擴展指令集

  set表示此函數的含義縮寫

 ps1表示該函數對結果變量的影響,由兩個字母組成,第一個字母表示對結果變量的影響方式,p表示把結果做爲指向一組數據的指針,每一個元素都將參與運算S表示只將結果變量中的第一個元素參與運算第二個字母表示參與運算的數據類型。s表示32位浮點數,d表示64位浮點數,i32表示32位定點數,i64表示64位定點數,由於SSE只支持32位浮點數的運算,所以你可能會在這些指令封裝函數中找不到包含非s修飾符的,但你可以在MMX和SSE2的指令集中去認識它們。

 接下來我舉一個例子來說明SSE的指令函數是如何使用的,必須要說明的是我以下的代碼都是在VC7.1的平臺上寫的,不保證對其它如Dev-C++、Borland C++等開發平臺的完全兼容。

 爲了方便對比速度,我會用常歸方法和SSE優化兩種寫法寫出,並會用一個測試速度的類CTimer來進行計時。

 這個算法是對一組float值進行放大,函數ScaleValue1是使用SSE指令優化的,函數ScaleValue2則沒有。我們用10000個元素的float數組數據來測試這兩個算法,每個算法運算10000遍,下面是測試程序和結果:

 

#include <xmmintrin.h>

#include <windows.h>

 

 

class CTimer

{

public:

        __forceinline CTimer( void )

       {

               QueryPerformanceFrequency( &m_Frequency );

               QueryPerformanceCounter( &m_StartCount );

       }

        __forceinline void Reset( void )

       {

               QueryPerformanceCounter( &m_StartCount );

       }

        __forceinline double End( void )

       {

               static __int64 nCurCount;

               QueryPerformanceCounter( (PLARGE_INTEGER)&nCurCount );

               return double( nCurCount * ( *(__int64*)&m_StartCount ) ) / double(  *(__int64*)&m_Frequency );

       }

private:

        LARGE_INTEGER m_Frequency;

        LARGE_INTEGER m_StartCount;

};

 

//使用SSE的函數

void ScaleValue1( float *pArray, DWORD  dwCount, float fScale )

{

 

 

      DWORD  dwGroupCount = dwCount / 4;

       __m128  e_Scale = _mm_set_ps1( fScale );

       for (  DWORD i = 0; i < dwGroupCount; i++ )

       {

               *(__m128*)( pArray + i * 4 ) = _mm_mul_ps( *(__m128*)( pArray + i * 4 ),  e_Scale );

       }

}

 

//不使用SSE的函數

void ScaleValue2( float *pArray, DWORD  dwCount, float fScale )

{

       for (  DWORD i = 0; i < dwCount; i++ )

       {

               pArray[i] *= fScale;

       }

}

#define ARRAYCOUNT 10000

int __cdecl main()

{

      //設置數據按16字節對齊 

      float  __declspec(align(16)) Array[ARRAYCOUNT];

        memset( Array, 0, sizeof(float) * ARRAYCOUNT );

       CTimer  t;

       double  dTime;

        t.Reset();

 

 

     for ( int i = 0; i  < 100000; i++ )

       {

               ScaleValue1( Array, ARRAYCOUNT, 1000.0f  );

       }

       dTime  = t.End();

       cout  << "Use SSE:" << dTime << "秒"  << endl;

        t.Reset();

       for (  int i = 0; i < 100000; i++ )

       {

               ScaleValue2( Array, ARRAYCOUNT, 1000.0f  );

       }

       dTime  = t.End();

       cout  << "Not Use SSE:" << dTime << "秒"  << endl;

        system( "pause" );

       return  0;

}

Use SSE:0.997817

Not Use SSE:2.84963


 
這裏要注意一下,我使用了__declspec(align(16))做爲數組定義的修釋符,這表示該數組是以16字節爲邊界對齊的,因爲SSE指令只能支持這種格式的內存數據

我的第一個SSE算法

分類:並行開發技術--SSE2012-03-28 16:5414人閱讀評論(0)收藏 舉報

/*************************************************
*
文件名:
SSEProject.cpp
*
版本:
0.0
*
功能描述:利用SSE函數優化浮點數據相乘的算法

*
創建日期:2012-03-28
*作者:
Brandy Yin
*
修改記錄:

*
備註:我的第一個SSE算法
*************************************************/
#include <windows.h>
#include <xmmintrin.h>
#include<iostream>
#include "Time.h"
using std::cout;
using std::endl;
#define ArrayCount 1000

//
使用SSE的函數
void f1(float* i_array,DWORD i_num,float i_mul)
{
DWORD num=i_num/4;
__m128 mul=_mm_set_ps1(i_mul);
for (int i=0;i!=num;i++)
{
*(__m128*)(i_array+i*4)=_mm_mul_ps(*(__m128*)(i_array+i*4),mul);
}
}

//
不使用SSE的函數
void f2(float* i_array,DWORD i_num,float i_mul)
{
for (int i=0;i!=i_num;i++)
{
*(i_array+i)=(*(i_array+i))*i_mul;
}
}


int main()
{
//
聲明數組在內存中按16字節對齊
float _declspec(align(16)) Array[ArrayCount];

//
利用memset函數初始化數組
memset(Array,0,sizeof(float)*ArrayCount);

CTime t;
t.Reset();
for (int i=0;i!=100000;i++)
{
f1(Array,ArrayCount,1000.2f);
}
cout<<"Use SSE: "<<t.End()<<"ms"<<endl;

t.Reset();
for (int i=0;i!=100000;i++)
{
f2(Array,ArrayCount,1000.2f);
}
cout<<"Don't Use SSE:"<<t.End()<<"ms"<<endl;

system("pause");
return 0;

}

 

作爲一個SSE初學者,我在寫這個算法時遇到了以下問題:

1)什麼是SSE----簡單地說,他是一組CPU指令集,VC++.net爲我們提供了很方便的指令C函數級的封裝和C格式數據類型,我們只需像平時寫C++代碼一樣定義變量、調用函數就可以很好的應用SSE指令了。

參考:http://blog.csdn.net/miss_acha/article/details/7403446

2)什麼是“__m128”類型?----SSE算法標準的數據類型。

它的定義參考:http://blog.csdn.net/miss_acha/article/details/7403446

3)函數調用方式?----這實際上是在看網上的一篇相關文章時接觸到的,就看了看,覺得還不錯。

參考:http://blog.csdn.net/miss_acha/article/details/7402728

4)內存對齊問題是怎麼回事?

參考:http://blog.csdn.net/miss_acha/article/details/7402896

5memset函數的使用?

參考:http://www.iteye.com/topic/353769

參考:http://blog.csdn.net/yangsen2016/article/details/1638503

利用SSE編程之前該思考的幾個問題

分類:並行開發技術--SSE2012-04-13 09:1515人閱讀評論(0)收藏 舉報

最近做的項目要用SSE優化,拿到手的程序跑下來要花一兩個小時,運算量相當大,不得不優化。下面來談一談從這次項目中總結到的幾點經驗,主要是關於在利用SSE優化之前我們該思考的幾個問題,也就是什麼情況下才合適用SSE優化:

 

1是否存在大量浮點型數據的運算SSE指令包括,單指令多數據浮點計算、以及額外的SIMD整數和高速緩存控制指令)。

 

2每個數據和對應的運算結果是否獨立與其他數據(獨立是最簡單的情況,有些情況雖然不獨立,但也可以通過一些方式做轉換,但不是所有的地方都合適用SSE優化,具體要到實際問題中學習了)。

 

3是否有尾數需要單獨處理(如果要處理的數據個數不是4的倍數,最後肯定存在幾個數據需要單獨考慮,爲了通用性,最好總是添加尾數處理)。

 

以上是在利用SSE優化之前需要考慮的幾個問題,在具體使用SSE指令的時候還有一些技巧,這個將會在後續文章中提到,尤其是關於每個指令本身的耗時問題,我們要儘量避免使用太耗時的指令。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章