[C/C++] ccpuid:CPUID信息模塊 V1.03版,改進mmx/sse指令可用性檢查(使用signal、setjmp,支持純C)、修正AVX檢查Bug


作者:zyl910

  之前的ccpuid V1.02的mmx/sse指令可用性檢查存在缺陷。現在的V1.03版改進了mmx/sse指令可用性檢查,使用signal、setjmp,能夠支持純C程序。修正了AVX檢查Bug。增加多文件鏈接ccpuid的測試例程。


一、更新說明

1.1 改進mmx/sse指令可用性檢查

1.1.1 問題背景

  以前是使用結構化異常處理來確認當前環境是否能運行mmx/sse指令的。該方法存在兩個問題。
  首先,僅有C++支持結構化異常處理,而純C是不支持的。所以在V1.02版中,是根據__cplusplus宏來做條件編譯的。造成僅有C++版支持指令可用性檢查,而純C版沒有該檢查能力。
  更重大的問題是——結構化異常處理並不能捕獲mmx/sse指令錯誤,程序會崩潰。
  這是因為——當運行了當前環境不支持指令時,不會觸發結構化異常處理,而是觸發SIG_ILL(非法指令)信號,若沒有該信號的處理函數,程序就崩潰了。

  本來我以為“硬件支持MMX/SSE,但當前運行環境不支持”這種情況是很罕見的,因為現在的主流操作系統(Windows、Linux、Mac OS X等)都是支持mmx和sse。
  但后來發現,這種情況是很容易出現的。這是因為AVX指令集的影響。
  當gcc用上“-mavx”編譯參數時,gcc會將所有的SSE代碼編譯為“VEX前綴編碼的SSE指令”,而不是“傳統SSE編碼的指令”。“VEX前綴編碼的SSE指令”實際上是128位的AVX指令,只有CPU硬件支持AVX,並且操作系統支持AVX時,才能運行“VEX前綴編碼的SSE指令”。
  就算你的代碼中沒有使用SSE,但是由於編譯器的自動矢量化編譯優化,也可能會自動生成VEX SSE指令,導致程序崩潰。

  而VC在這方面好一點,依然是生成傳統編碼的SSE指令,程序能夠在非AVX環境下運行。


1.1.2 解決辦法

  怎么捕獲SIG_ILL(非法指令)信號呢?C標准庫提供了<signal.h>,用於處理信號。
  從SIG_ILL信號中怎么恢復呢?C標准庫提供了<setjmp.h>,可以實現非局部轉移。

  但是還存在一個問題——在信號處理函數中,怎么知道應該回到哪一個位置呢?這時只有靠一個全局變量來存放返回位置。
  而使用全局變量,可能會引起線程安全問題。暫時不管了,目前的跨平台多線程處理很麻煩。還是等C11普及后再說吧。


1.1.3 具體實現1(修改ccpuid.h)

  在具體實現時,會遇到這個問題——純C版的ccpuid只是一個頭文件,按照慣例只能含有聲明性內容,不能含有定義性內容。而信號處理函數必須是一個實際定義的函數,這樣才能獲取它的函數指針進行注冊。
  如果再添加一個ccpuid.c源文件,在它里面編寫信號處理函數。雖然這樣更符合C語言慣例,但這樣用起來很麻煩,而且可能還需要修改代碼才能與新版的ccpuid兼容。
  所以我決定還是將信號處理函數及相關的全局變量放在頭文件中。為了避免外部暴露,可以加上static關鍵字,使它們僅在文件內可見、不參與鏈接。

  由於現在需要檢查MMX與SSE這兩類指令,所以可以考慮將這種檢查封裝成為一個函數——

// 收到SIGILL信號時的跳回地址.
static jmp_buf *volatile simd_pjmpback_sigill = NULL;

// 處理SIGILL信號.
static void simd_catch_sigill(int sig)
{
    jmp_buf * pjmp = simd_pjmpback_sigill;
    // 能夠跳回.
    if (NULL!=pjmp)
    {
        longjmp(*pjmp, 1);    // 跳回.
    }
    // 不能跳回.
    //fprintf(stderr, "!SIGILL!");
    abort();
}

// 嘗試調用一個可能會發生SIGILL信號的函數.
//
// result: 若正常運行,就返回pfunc的返回值. 否則返回0.
// pfunc: 欲測試的函數.
// puserdata: 用戶自定參數, 在調用pfunc時會傳遞該參數.
static int simd_try_sigill(int (*pfunc)(void*), void* puserdata)
{
    int rt = 0;
    jmp_buf myjmp;
    jmp_buf* poldjmp=NULL;    // 以前的跳回地址.
    void (*old_signal)(int) = SIG_DFL;    // 以前的信號處理函數.

    // 注冊跳轉.
    if (0==setjmp(myjmp))
    {
        // 登記跳回.
        poldjmp = simd_pjmpback_sigill;
        simd_pjmpback_sigill = &myjmp;

        // 注冊信號處理函數.
        old_signal = signal(SIGILL, simd_catch_sigill);

        // [try]
        rt = pfunc(puserdata);
    }
    else
    {
        // [catch]
    }

    // 恢復信號處理函數.
    simd_pjmpback_sigill = poldjmp;
    signal(SIGILL, old_signal);

    return rt;
}

 

 

  驗證MMX指令是否能運行——

// 驗證mmx指令是否能運行_實際指令測試.
static int    simd_try_mmx_pfunc(void* puserdata)
{
    #if defined(_M_X64) && defined(_MSC_VER) && !defined(__INTEL_COMPILER)
        // VC編譯器不支持64位下的MMX.
        return 0;
    #else
        _mm_empty();    // MMX instruction: emms
        return 1;
    #endif    // #if defined(_M_X64) && defined(_MSC_VER)
}

// 驗證mmx指令是否能運行.
static int    simd_try_mmx()
{
    int rt = 0;
    #if defined(_M_X64) && defined(_MSC_VER) && !defined(__INTEL_COMPILER)
        // VC編譯器不支持64位下的MMX.
    #else
        rt = simd_try_sigill(simd_try_mmx_pfunc, 0);
    #endif    // #if defined(_M_X64) && defined(_MSC_VER)
    return rt;
}

 

  驗證SSE指令是否能運行——

// 驗證sse指令是否能運行_實際指令測試.
static int    simd_try_sse_pfunc(void* puserdata)
{
    int rt = 0;
    volatile __m128 xmm1 = _mm_setzero_ps();    // SSE instruction: xorps
    int* pxmm1 = (int*)&xmm1;    // 避免GCC的 -Wstrict-aliasing 警告.
    if (0==*pxmm1)    rt = 1;    // 避免Release模式編譯優化時剔除_mm_setzero_ps.
    return rt;
}

// 驗證sse指令是否能運行.
static int    simd_try_sse()
{
    int rt = simd_try_sigill(simd_try_sse_pfunc, 0);
    return rt;
}

 

  然后再修改原來的simd_mmx、simd_sse_level函數,在最后調用simd_try_mmx、simd_try_sse驗證當前環境——

// 是否支持MMX指令集.
//
// result: 返回當前運行環境是否支持MMX指令集. 非0表示支持, 0表示不支持.
// phwmmx: 返回硬件是否支持MMX指令集. 非0表示支持, 0表示不支持.
INLINE int    simd_mmx(int* phwmmx)
{
    int    rt = 0;    // result
    #ifdef CCPUID_X86
        const uint32_t    BIT_D_MMX = 0x00800000;    // bit 23
        uint32_t dwBuf[4];

        // check processor support
        getcpuid(dwBuf, 1);    // Function 1: Feature Information
        if ( dwBuf[3] & BIT_D_MMX )    rt=1;
        if (NULL!=phwmmx)    *phwmmx=rt;

        // check OS support
        if ( rt>0 )
        {
            if (!simd_try_mmx()) rt=0;
        }
    #else    // #ifdef CCPUID_X86
        if (NULL!=phwmmx)    *phwmmx=rt;
    #endif    // #ifdef CCPUID_X86
    return rt;
}

// 檢測SSE系列指令集的支持級別.
//
// result: 返回當前運行環境的SSE系列指令集支持級別. 詳見SIMD_SSE_常數.
// phwmmx: 返回硬件的SSE系列指令集支持級別. 詳見SIMD_SSE_常數.
INLINE int    simd_sse_level(int* phwsse)
{
    int    rt = SIMD_SSE_NONE;    // result
    #ifdef CCPUID_X86
        const uint32_t    BIT_D_SSE = 0x02000000;    // bit 25
        const uint32_t    BIT_D_SSE2 = 0x04000000;    // bit 26
        const uint32_t    BIT_C_SSE3 = 0x00000001;    // bit 0
        const uint32_t    BIT_C_SSSE3 = 0x00000100;    // bit 9
        const uint32_t    BIT_C_SSE41 = 0x00080000;    // bit 19
        const uint32_t    BIT_C_SSE42 = 0x00100000;    // bit 20
        uint32_t dwBuf[4];

        // check processor support
        getcpuid(dwBuf, 1);    // Function 1: Feature Information
        if ( dwBuf[3] & BIT_D_SSE )
        {
            rt = SIMD_SSE_1;
            if ( dwBuf[3] & BIT_D_SSE2 )
            {
                rt = SIMD_SSE_2;
                if ( dwBuf[2] & BIT_C_SSE3 )
                {
                    rt = SIMD_SSE_3;
                    if ( dwBuf[2] & BIT_C_SSSE3 )
                    {
                        rt = SIMD_SSE_3S;
                        if ( dwBuf[2] & BIT_C_SSE41 )
                        {
                            rt = SIMD_SSE_41;
                            if ( dwBuf[2] & BIT_C_SSE42 )
                            {
                                rt = SIMD_SSE_42;
                            }
                        }
                    }
                }
            }
        }
        if (NULL!=phwsse)    *phwsse=rt;

        // check OS support
        if ( rt>0 )
        {
            if (!simd_try_sse()) rt=SIMD_SSE_NONE;
        }
    #else    // #ifdef CCPUID_X86
        if (NULL!=phwmmx)    *phwmmx=rt;
    #endif    // #ifdef CCPUID_X86
    return rt;
}

 


1.1.4 具體實現2(修改ccpuid.hpp、ccpuid.cpp)

  再考慮一下如何修改CCPUID類。原先是在調用RefreshProperty函數時,就一起更新_mmx、_sse等變量。而現在simd_try_mmx、simd_try_sse等驗證函數不是線程安全的,如果還是一起更新的話,恐怕會影響穩定性。
  所以設計為在需要時才驗證的模式更好。具體做法是,simd_mmx、simd_sse_level返回-1,由mmx、sse負責檢查。

  mmx、sse原先設計為 const成員函數,而現在需要保存驗證結果而修改_mmx、_sse變量,該怎么辦呢?有兩種方案——
1. 去掉該函數的const關鍵字,使其成為普通成員函數。
2. 將_mmx、_sse變量加上mutable關鍵字,使其能在 const成員函數 中修改。

  考慮到舊代碼的兼容性,我選擇了方案2。

  修改ccpuid.hpp:給_mmx、_sse變量加上mutable關鍵字,將mmx、sse函數挪至cpp文件中——

    int mmx() const;    // 系統支持MMX.
    int sse() const;    // 系統支持SSE.
...
    mutable int _mmx;    // 系統支持MMX.
    mutable int _sse;    // 系統支持SSE.

 



  修改ccpuid.cpp:simd_mmx、simd_sse_level返回-1,由mmx、sse負責檢查——

int    CCPUID::simd_mmx(int* phwmmx) const
{
    int    rt = 0;    // result
    #ifdef CCPUID_X86
        const uint32_t    BIT_D_MMX = 0x00800000;    // bit 23
        uint32_t dwBuf[4];

        // check processor support
        GetData(dwBuf, 1);    // Function 1: Feature Information
        if ( dwBuf[3] & BIT_D_MMX )    rt=1;
        if (NULL!=phwmmx)    *phwmmx=rt;

        // check OS support
        rt = -1;    // 需要時才檢查, 見mmx().
    #else    // #ifdef CCPUID_X86
        if (NULL!=phwmmx)    *phwmmx=rt;
    #endif    // #ifdef CCPUID_X86
    return rt;
}

int    CCPUID::simd_sse_level(int* phwsse) const
{
    int    rt = SIMD_SSE_NONE;    // result
    #ifdef CCPUID_X86
        const uint32_t    BIT_D_SSE = 0x02000000;    // bit 25
        const uint32_t    BIT_D_SSE2 = 0x04000000;    // bit 26
        const uint32_t    BIT_C_SSE3 = 0x00000001;    // bit 0
        const uint32_t    BIT_C_SSSE3 = 0x00000100;    // bit 9
        const uint32_t    BIT_C_SSE41 = 0x00080000;    // bit 19
        const uint32_t    BIT_C_SSE42 = 0x00100000;    // bit 20
        uint32_t dwBuf[4];

        // check processor support
        GetData(dwBuf, 1);    // Function 1: Feature Information
        if ( dwBuf[3] & BIT_D_SSE )
        {
            rt = SIMD_SSE_1;
            if ( dwBuf[3] & BIT_D_SSE2 )
            {
                rt = SIMD_SSE_2;
                if ( dwBuf[2] & BIT_C_SSE3 )
                {
                    rt = SIMD_SSE_3;
                    if ( dwBuf[2] & BIT_C_SSSE3 )
                    {
                        rt = SIMD_SSE_3S;
                        if ( dwBuf[2] & BIT_C_SSE41 )
                        {
                            rt = SIMD_SSE_41;
                            if ( dwBuf[2] & BIT_C_SSE42 )
                            {
                                rt = SIMD_SSE_42;
                            }
                        }
                    }
                }
            }
        }
        if (NULL!=phwsse)    *phwsse=rt;

        // check OS support
        rt = -1;    // 需要時才檢查, 見sse().
    #else    // #ifdef CCPUID_X86
        if (NULL!=phwsse)    *phwsse=rt;
    #endif    // #ifdef CCPUID_X86
    return rt;
}

...


int CCPUID::mmx() const
{
    if (_mmx<0)
    {
        // 發現未檢查, 進行檢查.
        _mmx = _hwmmx;
        if (_mmx>0)
        {
            if (!simd_try_mmx()) _mmx=0;
        }
    }
    return _mmx;
}

int CCPUID::sse() const
{
    if (_sse<0)
    {
        // 發現未檢查, 進行檢查.
        _sse = _hwsse;
        if (_sse>0)
        {
            if (!simd_try_sse()) _sse=SIMD_SSE_NONE;
        }
    }
    return _sse;
}

 

 

1.2 修正AVX檢查Bug

  V1.02版因X86平台判斷功能改動了AVX檢查代碼,不小心留下了一個Bug——當操作系統不支持OSXSAVE時,simd_avx_level函數卻返回一個正數,報告當前環境支持AVX。而按照規定,它應該返回0。

  經過檢查,發現是條件判斷覆蓋問題。於是修改了 simd_avx_level、CCPUID::simd_avx_level,修正了此Bug。


1.3 增加多文件鏈接ccpuid的測試例程

  因SIGILL信號處理,現在ccpuid.h這個頭文件中不得不含有定義性代碼。一般來說,當頭文件中含有定義性代碼時,有時會造成多文件鏈接時報錯。
  雖然我們已經加上了static關鍵字使那部分代碼變為僅文件內可見、不參與鏈接。但是為了驗證,還是編寫一個測試例程比較好。

  於是在C++測試例程項目中 增加兩個文件——test2.h和test2.cpp。
  test2.h用於聲明test2函數——
void test2(void);

  在test2.cpp中也引用了ccpuid.hpp,並寫了簡單的測試代碼——

#include <stdio.h>

#include "ccpuid.hpp"

#include "test2.h"

// 測試調用 const對象 的 const成員函數.
void test2_const(const CCPUID& ccid)
{
    printf("test2_SSE:\t%d\n",ccid.sse());
}

// 測試多文件鏈接使用ccpuid.
void test2(void)
{
    test2_const(CCPUID::cur());
}

 

  最后在testccpuid.cpp中添加對test2函數的調用——

#include "test2.h"
...
test2();

 

 

二、全部代碼

(略)

2.1 ccpuid模塊

2.1.1 ccpuid.h: CPUID信息(純C版)

  全部代碼——


2.1.2 ccpuid.hpp: CPUID信息(C++版頭文件)

  全部代碼——


2.1.3 ccpuid.cpp: CPUID信息(C++版源文件)

  全部代碼——


2.2 純C版測試例程

2.2.1 testccpuidc.c : [C] 測試ccpuid.h, 顯示CPUID信息

  全部代碼——


2.3 C++版測試例程

2.3.1 testccpuid.cpp : [C++] 測試ccpuid.hpp, 顯示所有的CPUID信息

  全部代碼——


2.3.2 test2.h: 多文件鏈接ccpuid的測試(頭文件)

  全部代碼——


2.3.3 test2.cpp: 多文件鏈接ccpuid的測試(源文件)

  全部代碼——


2.4 makefile

  makefile——


三、編譯測試

  在以下編譯器中成功編譯——
VC6:x86版。
VC2003:x86版。
VC2005:x86版。
VC2010:x86版、x64版。
GCC 4.7.0(Fedora 17 x64):x86版、x64版。
GCC 4.6.2(MinGW (20120426)):x86版。
GCC 4.7.1(TDM-GCC (MinGW-w64)):x86版、x64版。
llvm-gcc-4.2(Mac OS X Lion 10.7.4, Xcode 4.4.1):x86版、x64版。

 

 

參考文獻——
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1, 2A, 2B, 2C, 3A, 3B, and 3C》044US. August 2012. http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
《Intel® Architecture Instruction Set Extensions Programming Reference》014. AUGUST 2012. http://software.intel.com/en-us/avx/
《Intel® Processor Identification and the CPUID Instruction》. May 2012. http://developer.intel.com/content/www/us/en/processors/processor-identification-cpuid-instruction-note.html
《AMD64 Architecture Programmer's Manual Volume 3: General Purpose and System Instructions》. December 2011. http://support.amd.com/us/Processor_TechDocs/24594_APM_v3.pdf
《AMD CPUID Specification》. September 2010. http://support.amd.com/us/Embedded_TechDocs/25481.pdf
《x86 architecture CPUID》. http://www.sandpile.org/x86/cpuid.htm
《Haswell New Instruction Descriptions Now Available! 》. Mark Buxton. http://software.intel.com/en-us/blogs/2011/06/13/haswell-new-instruction-descriptions-now-available/
[IDF2012]ARCS002《Introduction to the upcoming Intel® Advanced Vector Extensions 2 (Intel® AVX2)》. 王有偉, Henry Ou. 2012-4.
[IDF2012]ARCS002《即將推出的英特爾® 高級矢量擴展指令集2(英特爾® AVX2)介紹》. 王有偉, Henry Ou. 2012-4.
《x86/x64 指令系統》. mik(鄧志). http://www.mouseos.com/x64/default.html
《[x86]SIMD指令集發展歷程表(MMX、SSE、AVX等)》. http://www.cnblogs.com/zyl910/archive/2012/02/26/x86_simd_table.html
《如何在各個版本的VC及64位下使用CPUID指令》. http://www.cnblogs.com/zyl910/archive/2012/05/21/vcgetcpuid.html
《[VC兼容32位和64位] 檢查MMX和SSE系列指令集的支持級別》. http://www.cnblogs.com/zyl910/archive/2012/05/25/checksimd64.html
《[VC] CPUIDFIELD:CPUID字段的統一編號、讀取方案。范例:檢查SSE4A、AES、PCLMULQDQ指令》. http://www.cnblogs.com/zyl910/archive/2012/06/29/getcpuidfield.html
《[VC] 檢測AVX系列指令集的支持級別(AVX、AVX2、F16C、FMA、FMA4、XOP)》. http://www.cnblogs.com/zyl910/archive/2012/07/04/checkavx.html
《[C#] cmdarg_ui:“簡單參數命令行程序”的通用圖形界面》.  http://www.cnblogs.com/zyl910/archive/2012/06/19/cmdarg_ui.html
《[C/C++] 顯示各種C/C++編譯器的預定義宏(C11標准、C++11標准、VC、BCB、Intel、GCC)》. http://www.cnblogs.com/zyl910/archive/2012/08/02/printmacro.html
《[C] 讓VC、BCB支持C99的整數類型(stdint.h、inttypes.h)(兼容GCC)》. http://www.cnblogs.com/zyl910/archive/2012/08/08/c99int.html
《GCC 64位程序的makefile條件編譯心得——32位版與64位版、debug版與release版(兼容MinGW、TDM-GCC)》. http://www.cnblogs.com/zyl910/archive/2012/08/14/gcc64_make.html
《[C] 在GCC中獲取CPUID信息(兼容VC)》. http://www.cnblogs.com/zyl910/archive/2012/08/06/getcpuid_gcc.html
《ccpuid:CPUID信息模塊。范例:顯示所有的CPUID信息》. http://www.cnblogs.com/zyl910/archive/2012/07/11/ccpuid.html
《ccpuid:CPUID信息模塊 V1.01版,支持GCC(兼容32位或64位的Windows/Linux)》. http://www.cnblogs.com/zyl910/archive/2012/08/22/ccpuid_v101.html
《ccpuid:CPUID信息模塊 V1.02版,支持Mac OS X,支持純C,增加CPUF常數》. http://www.cnblogs.com/zyl910/archive/2012/09/29/ccpuid_v102.html


源碼下載——
http://files.cnblogs.com/zyl910/ccpuid_v103.rar


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM