環(huán)球觀焦點(diǎn):[c++實(shí)踐]內(nèi)存對(duì)齊與偽共享

2023-01-09 16:26:08 來(lái)源:51CTO博客

內(nèi)存對(duì)齊與偽共享

時(shí)間測(cè)試類

該類會(huì)在后續(xù)的測(cè)試中用于運(yùn)行時(shí)間測(cè)試。

// public/timer.h#include #include #include struct ScopeTimer{    ScopeTimer(const char *msg):_msg(msg),_now(std::chrono::high_resolution_clock::now()){}    ~ScopeTimer(){        std::cout << _msg << ",espaced " << std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - _now).count() << "ms" << std::endl;    }    const char *_msg;    std::chrono::high_resolution_clock::time_point _now;};void test_cycle(std::function f,long cycle,const char *msg){    ScopeTimer s(msg);    for (long index = 0;index < cycle;++index)    {        f();    }}

內(nèi)存對(duì)齊

內(nèi)存對(duì)齊規(guī)則

我們知道,在我們寫(xiě)結(jié)構(gòu)體時(shí),默認(rèn)情況下編譯器會(huì)自動(dòng)對(duì)這些數(shù)據(jù)結(jié)構(gòu)進(jìn)行對(duì)齊,對(duì)齊的規(guī)則為按照最大的??pod??類型進(jìn)行對(duì)齊。同時(shí),我們也可以強(qiáng)制指定對(duì)齊的大小,此時(shí)將按照默認(rèn)與指定的字節(jié)中較小的進(jìn)行對(duì)齊。在程序員看來(lái),內(nèi)存是一個(gè)一個(gè)字節(jié)的,但是現(xiàn)代操作系統(tǒng)在進(jìn)行內(nèi)存管理時(shí)會(huì)要求內(nèi)存按照N字節(jié)進(jìn)行對(duì)齊,一般是??4??字節(jié)。假設(shè)有如下幾個(gè)結(jié)構(gòu)體,按照不同的對(duì)齊規(guī)則,有不同的大小。

#include // 使用默認(rèn)對(duì)齊struct t1{    char  c;    int   a;    char  b;    short d;};// 強(qiáng)制1字節(jié)對(duì)齊#pragma pack(1)struct t2{    char  c;    int   a;    char  b;    short d;};struct t3{    char  c;    int   a;    short d;};// 恢復(fù)默認(rèn)對(duì)齊#pragma pack()int main(){    std::cout << "sizeof(t1):" << sizeof(t1) << std::endl; // 12    std::cout << "sizeof(t2):" << sizeof(t2) << std::endl; // 8    std::cout << "sizeof(t3):" << sizeof(t3) << std::endl; // 7    return 0;}

內(nèi)存對(duì)齊對(duì)讀取次數(shù)的影響

同樣的,對(duì)于上面的2個(gè)結(jié)構(gòu)體??t1??和??t2??,在要訪問(wèn)成員變量??a??時(shí),內(nèi)存訪問(wèn)的次數(shù)是不一樣的。對(duì)于??t1??,由于是??4??字節(jié)對(duì)齊,??t1.a??本身就是符合內(nèi)存對(duì)齊要求的,因此只需要一次存取。對(duì)于??t2??,由于是??1??字節(jié)強(qiáng)制對(duì)齊,??t2.a??在內(nèi)存中的布局如下:


(相關(guān)資料圖)

struct t2{    char  c;  // 0    int   a;  // 1、2、3、4    char  b;  // 5    short d;  // 6、7};

要訪問(wèn)??t2.a??,首先要將??0~3??字節(jié)和??4~7??字節(jié)分??2??次讀取到內(nèi)存中,然后再?gòu)??1~4??字節(jié)獲取變量??t2.a??的值,因此未對(duì)齊的數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)速度要比對(duì)齊的數(shù)據(jù)結(jié)構(gòu)訪問(wèn)次數(shù)慢不止2倍。經(jīng)過(guò)測(cè)試,實(shí)際的訪問(wèn)速度基本沒(méi)有差別,是測(cè)試代碼有問(wèn)題??測(cè)試代碼如下:

#include #include #include #include "../public/timer.h"http:// 使用默認(rèn)對(duì)齊struct t1{    char  c{0};    int   a{0};    char  b{0};    short d{0};};// 強(qiáng)制1字節(jié)對(duì)齊#pragma pack(1)struct t2{    char  c{0};    int   a{0};    char  b{0};    short d{0};};#pragma pack()long cycle{10000};int main(){    std::thread x1(        [](){            struct t1 a;            ScopeTimer s("inc t1.a");            for (long index = 0;index < cycle;++index)            {                a.a++;                std::this_thread::sleep_for(std::chrono::microseconds(1));            }        }    );    std::thread x2([](){        struct t2 a;        ScopeTimer s("inc t2.a");        for (long index = 0;index < cycle;++index)        {            a.a++;            std::this_thread::sleep_for(std::chrono::microseconds(1));        }    });    x1.join();    x2.join();    return 0;}//inc t2.a,espaced 7110ms//inc t1.a,espaced 7115ms

嗯,這是由于??cache line??造成的嗎??還是說(shuō)內(nèi)存對(duì)齊與否造成的影響本身是可以忽略的??后續(xù)在介紹??cpu cache line??之后,在排除了緩存命中問(wèn)題后,使用數(shù)組來(lái)再次測(cè)試內(nèi)存對(duì)齊與否的性能差異。

cpu cache line

現(xiàn)代??cpu??都帶有緩存,一般分為3級(jí),離??cpu??越近的緩存存取速度越快,同時(shí)緩存的容量越小。現(xiàn)代??cpu??的一級(jí)緩存一般大小為??4~64k??,并且存取時(shí)是以??cache line??的形式進(jìn)行的。我們?nèi)粘J褂玫??cpu cache line??一般??64??字節(jié),也就是??cpu??在讀取數(shù)據(jù)時(shí)會(huì)一次性的從上級(jí)內(nèi)存將??64??字節(jié)的數(shù)據(jù)讀取到當(dāng)前緩存中。因此,當(dāng)我們要讀取一個(gè)??long??類型的數(shù)據(jù)時(shí),??cpu??實(shí)際上會(huì)將和它臨近的一些字節(jié)一起讀取到一級(jí)緩存中,以滿足一次讀取一個(gè)??cache line??的要求。

偽共享

如果??cpu??只有一個(gè)核,在多線程編程時(shí),每個(gè)線程進(jìn)行切換時(shí),都需要將當(dāng)前線程的上下文進(jìn)行保存,然后加載下次要運(yùn)行的線程的上下文,這就叫做上下文切換。現(xiàn)代??cpu??一般都會(huì)有多個(gè)核,因此實(shí)際運(yùn)行時(shí)會(huì)有多個(gè)線程并行運(yùn)行,每個(gè)核都有獨(dú)立的緩存,正常情況下并行運(yùn)行的??2??個(gè)線程如果沒(méi)有訪問(wèn)或者修改相同內(nèi)存是不會(huì)相互影響。但由于??cache line??的存在,如果一個(gè)線程修改了運(yùn)行在另外一個(gè)核上線程??cache line??上的某一數(shù)據(jù),則此時(shí)??cpu??需要重新加載該??cache line??上的數(shù)據(jù)。我們可以通過(guò)以下代碼來(lái)證明該現(xiàn)象的存在:

#include "../public/timer.h"#include struct Array{    long size{100};    long curIndex{0};};void incIndex(struct Array &arr){    arr.curIndex++;}void getSize(struct Array &arr){    long s = arr.size;}constexpr long maxIndex{100000000};int main(int argc,char **argv) {        struct Array arr;    {        ScopeTimer s("main");        long index{0};        while(index++ < maxIndex)        {            incIndex(arr);        }    }    std::thread t1([&arr](){        ScopeTimer s("thread1");        long index{0};        while(index++ < maxIndex)        {            incIndex(arr);        }    });    std::thread t2([&arr](){        ScopeTimer s("thread2");        long index{0};        while(index++ < maxIndex)        {            getSize(arr);        }    });    t1.join();    t2.join();    return 0;}//main,espaced 250ms//thread2,espaced 606ms//thread1,espaced 699ms

??main??函數(shù)中的輸出證明對(duì)??Array??的??N??次遞增只需要??250ms??,但是當(dāng)我們?cè)讵?dú)立線程中讓線程??1??對(duì)??curIndex??進(jìn)行遞增,讓線程??2??獲取??size??的值,可以看到他們的運(yùn)行時(shí)間都有答復(fù)提升。這是由于線程??1??和線程??2??分別運(yùn)行在不同的??cpu??核心上,線程??2??的??cpu??會(huì)同時(shí)將??curIndex??和??size??同時(shí)讀取到??cache line??,當(dāng)線程??1??修改了??curIndex??的時(shí)候,會(huì)造成線程??2??中的??curIndex??的值發(fā)生改變,雖然線程??2??不關(guān)心??curIndex??,但是此時(shí)??cpu??還是需要重新從內(nèi)存獲取整個(gè)??cache line??的數(shù)據(jù),因此造成運(yùn)行時(shí)間的大幅提升。如果在兩個(gè)線程都運(yùn)行??getSize??,可以看到兩個(gè)線程運(yùn)行的時(shí)間都在??200ms??左右。

解決偽共享

雖然兩個(gè)線程訪問(wèn)的數(shù)據(jù)是獨(dú)立的,但是可能會(huì)存在某一線程修改的數(shù)據(jù),在另外一個(gè)線程的??cache line??中,這樣會(huì)造成另外一個(gè)線程需要重新存取整個(gè)??cache line??,這種現(xiàn)象叫做偽共享。要解決偽共享,就要避免多個(gè)線程的??cache line??相互影響,此時(shí)我們可以通過(guò)強(qiáng)制補(bǔ)充不需要的數(shù)據(jù),讓我們要訪問(wèn)的數(shù)據(jù)相互隔離,避免??cache line??的影響。測(cè)試代碼如下:

#include "../public/timer.h"#include struct Array{    long size{100};    char padding[64-sizeof(long)];    long curIndex{0};};void incIndex(struct Array &arr){    arr.curIndex++;}void getSize(struct Array &arr){    long s = arr.size;}constexpr long maxIndex{100000000};int main(int argc,char **argv) {    struct Array arr;    {        ScopeTimer s("main incIndex");        long index{0};        while(index++ < maxIndex)        {            incIndex(arr);        }    }    {        ScopeTimer s("main getSize");        long index{0};        while(index++ < maxIndex)        {            getSize(arr);        }    }    std::thread t1([&arr](){        ScopeTimer s("thread1 incIndex");        long index{0};        while(index++ < maxIndex)        {            incIndex(arr);        }    });    std::thread t2([&arr](){        ScopeTimer s("thread2 getSize");        long index{0};        while(index++ < maxIndex)        {            getSize(arr);        }    });    t1.join();    t2.join();    return 0;}//main incIndex,espaced 244ms//main getSize,espaced 236ms//thread2 getSize,espaced 192ms//thread1 incIndex,espaced 259ms

由于我們?cè)谝L問(wèn)的數(shù)據(jù)中添加了??padding??,此時(shí)兩個(gè)線程要訪問(wèn)/修改的數(shù)據(jù)都是相互獨(dú)立的,可以看到運(yùn)行的時(shí)間基本和他們?cè)??main??函數(shù)中依次運(yùn)行時(shí)大致一致。

測(cè)試代碼

屏蔽cache line測(cè)試內(nèi)存對(duì)齊的影響

在??內(nèi)存對(duì)齊對(duì)讀取次數(shù)的影響??的測(cè)試中,我們看到對(duì)齊和不對(duì)齊實(shí)際的測(cè)試結(jié)果和我們預(yù)期不符,兩者的運(yùn)行時(shí)間大致一致。這可能是由于??cache line??的原因造成的。這里我們創(chuàng)建2個(gè)結(jié)構(gòu)體,大小都是??64??字節(jié),然后動(dòng)態(tài)創(chuàng)建數(shù)組,并對(duì)數(shù)組中的每個(gè)元素的處于??4??字節(jié)對(duì)齊位置和非對(duì)齊位置進(jìn)行累加,這樣可以避免由于??cache line??對(duì)同一元素進(jìn)行訪問(wèn)時(shí)由于緩存造成??N??次循環(huán)中實(shí)際只有第一次訪問(wèn)時(shí)是存在差異的。通過(guò)測(cè)試,我們可以看到內(nèi)存對(duì)齊的訪問(wèn)速度是非內(nèi)存對(duì)齊的??9??倍!!!

#include #include #include #include "../public/timer.h"http:// 使用默認(rèn)對(duì)齊struct t5{    char  c{0};    int   a{0};    char  b{0};    short d{0};    char  x[64-12];};// 強(qiáng)制1字節(jié)對(duì)齊#pragma pack(1)struct t6{    char  c;    int   a;    char  x[64-5];};#pragma pack()constexpr  long cycle{100000000};int main(){    std::cout << "sizeof(t5):" << sizeof(t5) << std::endl; // 64    std::cout << "sizeof(t6):" << sizeof(t6) << std::endl; // 64    // 提前申請(qǐng)內(nèi)存,避免內(nèi)存申請(qǐng)?jiān)斐傻牟町?   struct t5 * a1 = new struct t5[cycle];    struct t6 * a2 = new struct t6[cycle];    // 對(duì)內(nèi)存對(duì)齊的N個(gè)元素分別進(jìn)行1次訪問(wèn)    {        ScopeTimer s("inc t5.a");        for (long index = 0;index < cycle;++index)        {            a1[index].a++;        }    }    // 對(duì)非內(nèi)存對(duì)齊的N個(gè)元素分別進(jìn)行1次訪問(wèn)    {        ScopeTimer s("inc t6.a");        for (long index = 0;index < cycle;++index)        {            a2[index].a++;        }    }}//sizeof(t5):64//sizeof(t6):64//inc t5.a,espaced 568ms//inc t6.a,espaced 4611ms

標(biāo)簽: 運(yùn)行時(shí)間 數(shù)據(jù)結(jié)構(gòu) 另外一個(gè)

上一篇:世界觀察:Java工作流詳解(附6大工作流框架對(duì)比)?
下一篇:天天滾動(dòng):Linux下命令(3)