數據對齊


本文是針對2005年的一篇關於數據對齊的技術文章《Data alignment: Straighten up and fly right》的學習筆記。以下內容中理論部分來自對文章的翻譯,實驗部分是在魅族16x(高通驍龍sdm710)上跑的測試結果。

理論

內存訪問粒度

我們通常簡單地認為內存就像是一個字節數組,CPU逐字節逐字節地訪問內存:

然而事實上,CPU通常以2字節,4字節,8字節或16字節為塊,逐塊逐塊地訪問內存:

這個塊的大小(也就是一次讀取內存的大小)被叫做內存訪問粒度(Memory access granularity)。

數據對齊

我們所說的數據按2 byte對齊,4 byte對齊,8 byte 對齊,指的是數據的內存地址能被2,4,8整除。我們通過一個例子來看不同內存訪問粒度下,地址對齊與否在內存訪問上的差異。這個例子是:從地址0(對齊地址)開始讀取4字節,然后再從地址1(非對齊地址)開始讀取4字節。

  • 1 byte 內存粒度

因為一次就讀取1字節,所以不管從哪個地址開始讀取,都需要4次內存訪問。

  • 2 byte 內存粒度

從地址0開始讀取,需要2次內存訪問。而地址1是個非對齊的地址,因為它不是內存粒度的倍數,地址不在內存訪問的邊界,所以從地址1開始讀取需要:
首先讀取(0-1)兩個地址,將(0)移除;
其次讀取(2-3)兩個地址;
再次讀取(4-5)兩個地址,將(5)移除;
最后合並(1),(2-3)和(4),放入寄存器中。
可見從地址1開始讀取,需要3次內存訪問和移除之類的附加操作。

  • 4 byte 內存粒度

從地址0開始讀取,需要1次內存訪問。而從地址1開始讀取需要:
首先讀取(0-3)四個地址,將(0)移除;
其次讀取(4-7)四個地址,將(5-7)移除;
最后合並(1-3)和(4),放入寄存器中。
可見從地址1開始讀取,需要2次內存訪問和移除之類的附加操作。

鑒於內存訪問的開銷是一個常量,讀取同樣大小的字節時,內存粒度越大,內存訪問的開銷越小,讀取的速度就越快。地址不對齊時,需要更多內存訪問的開銷以及附加操作,從而降低了讀取速度。

結構體成員對齊

默認情況下:

  • 結構體中各成員按自身類型大小對齊,即各成員變量的地址必須是自身類型的倍數。
  • 結構體的總大小必須是成員中最大類型的整數倍,整個結構體的地址也必須是成員中最大類型的整數倍。

為了達到上述要求,各成員變量之間會補充相應的padding。如下例,結構體S1的成員m1m2之間有7 byte的填充,整個結構體的大小是16 byte。通過打印查看,s1 的地址是0xffda3d68, s1.m2的地址是0xffda3d70。要注意s1 的地址也必須是sizeof(double)的倍數,否則m1m2之間填充的就不是7 byte了。

struct S1
{
    char   m1;   // 1-byte
                 // padding 7-byte space here
    double m2;   // 8-byte
};

struct S1 s1;

printf("size of s1 %u", sizeof(s1));
printf("s1 address 0x%x, s1.m2 address 0x%x\n", &s1, &(s1.m2));
  • 結構體作為成員,類型大小按其成員所含最大類型計算。

如下例,結構體S1作為結構體S2的一個成員。因為S1中最大類型成員是m2,所以S2的成員s1按8 byte(sizeof(double))對齊而不是按16 byte(sizeof(struct S1))對齊,整個結構體S2的大小為24 byte。通過打印查看,s2的地址是0xffaf3b20, s2.s1的地址是0xffaf3b28。

struct S2
{
    char      m1;    // 1-byte
                     // padding 7-byte space here
    struct S1 s1;    // 16-byte
};

struct S2 s2;

printf("size of s2 %u\n", sizeof(s2));
printf("s2 address 0x%x, s2.s1 address 0x%x\n", &s2, &(s2.s1));

在某些場景下,我們想要控制結構體中各成員的字節對齊,那么可以通過#pragma pack宏來控制。如下例,#pragma pack(push, 1)表明結構體中各成員按1 byte對齊,結構體S1的大小為9,結構體S2的大小為10。通過打印,s2.s1的地址是0xffd1ccd1,顯然已經不能被2,4,8整除了。

#pragma pack(push, 1)
struct S1
{
    char   m1;
    double m2;
};
#pragma pack(pop)

struct S2
{
    char      m1;
    struct S1 s1;
};

實驗

速度

基於上面的理論,從一個非對齊的地址開始訪問內存會需要更多的開銷,原文《Data alignment: Straighten up and fly right》中也通過實驗對比了非對齊地址和對齊地址訪問內存的速度,實驗的結果是對齊地址訪問內存的速度要快過非對齊地址。考慮原文發表於2005年,其實驗結果是否仍然適用於如今不得而知,因此本文在魅族16x(高通驍龍sdm710)上進行相同的實驗,看看最近的CPU是否已經支持非對齊地址訪問。測試代碼如下:

void Munge8(void* data, size_t size)
{
    uint8_t* data8    = reinterpret_cast<uint8_t*>(data);
    uint8_t* data8end = data8 + size;

    while (data8 != data8end)
    {
         *data8 = -(*data8);
         ++data8;
    }
}

void Munge16(void* data, size_t size)
{
    uint16_t* data16    = reinterpret_cast<uint16_t*>(data);
    uint16_t* data16end = data16 + (size >> 1);

    uint8_t*  data8     = reinterpret_cast<uint8_t*>(data16end);
    uint8_t*  data8end  = data8 + (size & 0x0001);

    while (data16 != data16end)
    {
        *data16 = -(*data16);
        ++data16;
    }

    while (data8 != data8end)
    {
        *data8 = -(*data8);
        ++data8;
    }
}

void Munge32(void* data, size_t size)
{
    uint32_t* data32    = reinterpret_cast<uint32_t*>(data);
    uint32_t* data32end = data32 + (size >> 2);

    uint8_t*  data8     = reinterpret_cast<uint8_t*>(data32end);
    uint8_t*  data8end  = data8 + (size & 0x0003);

    while (data32 != data32end)
    {
        *data32 = -(*data32);
        ++data32;
    }

    while (data8 != data8end)
    {
        *data8 = -(*data8);
        ++data8;
    }
}

void Munge64(void* data, size_t size)
{
    uint64_t* data64    = reinterpret_cast<uint64_t*>(data);
    uint64_t* data64end = data64 + (size >> 3);

    uint8_t*  data8     = reinterpret_cast<uint8_t*>(data64end);
    uint8_t*  data8end  = data8 + (size & 0x0007);

    while (data64 != data64end)
    {
        *data64 = -(*data64);
        ++data64;
    }

    while (data8 != data8end)
    {
        *data8 = -(*data8);
        ++data8;
    }
}

void run_memory_alignment_test()
{
    char* data = new char[1024 * 1024 * 200]();
    if (NULL != data)
    {
        const size_t munge_size = 1024 * 1024 * 160;
        long long    tic        = 0;
        long long    toc        = 0;
        
        for (uint i = 0; i < 16; ++i)
        {
            void* pData = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(&data[0]) + i);
            printf("read data from address 0x%x\n", pData);
            
            // test for Munge8
            tic = Utils::GetCurrentTime();
            for (uint j = 0; j < 10; ++j)
            {
                Munge8(pData, munge_size);
            }
            toc = Utils::GetCurrentTime();

            printf("run Munge8 for align %u use %llums\n", i, (toc - tic) / 10);

            // test for Munge16
            tic = Utils::GetCurrentTime();
            for (uint j = 0; j < 10; ++j)
            {
                Munge16(pData, munge_size);
            }
            toc = Utils::GetCurrentTime();

            printf("run Munge16 for align %u use %llums\n", i, (toc - tic) / 10);

            // test for Munge32
            tic = Utils::GetCurrentTime();
            for (uint j = 0; j < 10; ++j)
            {
                Munge32(pData, munge_size);
            }
            toc = Utils::GetCurrentTime();

            printf("run Munge32 for align %u use %llums\n", i, (toc - tic) / 10);

            // test for Munge64
            tic = Utils::GetCurrentTime();
            for (uint j = 0; j < 10; ++j)
            {
                Munge64(pData, munge_size);
            }
            toc = Utils::GetCurrentTime();

            printf("run Munge64 for align %u use %llums\n", i, (toc - tic) / 10);
        }

        delete[] data;
        data = NULL;
    }
    else
    {
        printf("error : out of memory.");
    }
}

測試代碼以NDK分別基於armeabi,armeabi-v7a和arm64-v8a三種指令集編譯可執行文件。在高通驍龍sdm710上執行可執行文件,從實際的結果來看,運行時間已經和原文給出的結論不一樣,測試結果如下:

  • 基於armeabi指令集

從測試結果看,armeabi指令集上不管起始地址是否對齊,其耗時基本一致。同時還看出單次訪問的字節數越多,循環次數越少,整體的運行時間就越少,Time(Munge8) > Time(Munge16) > Time(Munge32) > Time(Munge64)。

另外當地址不是4的倍數時,運行Munge64會拋出總線錯誤的異常:

11-08 10:39:55.763  7325  7325 F libc    : Fatal signal 7 (SIGBUS), code 1, fault addr 0xe0300001 in tid 7325 (alignment_test), pid 7325 (alignment_test)
11-08 10:39:55.837  7328  7328 I crash_dump32: obtaining output fd from tombstoned, type: kDebuggerdTombstone
11-08 10:39:55.838  1005  1005 I /system/bin/tombstoned: received crash request for pid 7325
11-08 10:39:55.839  7328  7328 I crash_dump32: performing dump of process 7325 (target tid = 7325)
11-08 10:39:55.839  7328  7328 I crash_dump32: call setprop_coredump_comm_pid, 7325 (target tid = 7325)
11-08 10:39:55.841  7328  7328 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
11-08 10:39:55.841  7328  7328 F DEBUG   : Build fingerprint: 'Meizu/meizu_16X_CN/16X:8.1.0/OPM1.171019.026/1539636360:userdebug/test-keys'
11-08 10:39:55.841  7328  7328 F DEBUG   : Revision: '0'
11-08 10:39:55.841  7328  7328 F DEBUG   : ABI: 'arm'
11-08 10:39:55.841  7328  7328 F DEBUG   : pid: 7325, tid: 7325, name: alignment_test  >>> ./alignment_test <<<
11-08 10:39:55.841  7328  7328 F DEBUG   : signal 7 (SIGBUS), code 1 (BUS_ADRALN), fault addr 0xe0300001
11-08 10:39:55.841  7328  7328 F DEBUG   :     r0 e0300001  r1 00000000  r2 f2c80001  r3 fffffff8
11-08 10:39:55.841  7328  7328 F DEBUG   :     r4 0a000000  r5 f2c80001  r6 0a000000  r7 ff99f9d0
11-08 10:39:55.841  7328  7328 F DEBUG   :     r8 00000000  r9 00000000  sl 00000000  fp ff99fa0c
11-08 10:39:55.841  7328  7328 F DEBUG   :     ip 000ba572  sp ff99f988  lr b948fa97  pc b948f8ac  cpsr 20000030
11-08 10:39:55.843  7328  7328 F DEBUG   : 
11-08 10:39:55.843  7328  7328 F DEBUG   : backtrace:
11-08 10:39:55.843  7328  7328 F DEBUG   :     #00 pc 000008ac  /system/bin/alignment_test/armeabi/alignment_test
11-08 10:39:55.843  7328  7328 F DEBUG   :     #01 pc 00000a93  /system/bin/alignment_test/armeabi/alignment_test
11-08 10:39:55.843  7328  7328 F DEBUG   :     #02 pc 00000811  /system/bin/alignment_test/armeabi/alignment_test
11-08 10:39:55.843  7328  7328 F DEBUG   :     #03 pc 00080ba5  /system/lib/libc.so (__libc_init+48)
11-08 10:39:55.843  7328  7328 F DEBUG   :     #04 pc 000007c8  /system/bin/alignment_test/armeabi/alignment_test
11-08 10:39:55.891  7328  7328 I crash_dump32: performing dump name is ./alignment_test

從拋出的錯誤代碼BUS_ADRALN來看是因為地址不對齊引起的,但是不清楚為何地址不對齊時,Munge32等其他函數運行不會有總線錯誤出現,還希望有了解的大神指導指導!

  • 基於armeabi-v7a指令集

和armeabi指令集一樣,armeabi-v7a指令集上不管起始地址是否對齊,其耗時基本一致。雖然得益於armeabi-v7a指令集的優化,函數運行時間得到了提升,但依然遵循:Time(Munge8) > Time(Munge16) > Time(Munge32) > Time(Munge64)。同樣地,當地址不是4的倍數時,運行Munge64會拋出同樣的總線錯誤。

  • 基於arm64-v8a指令集

從測試結果看,arm64-v8a指令集上不管起始地址是否對齊,其耗時基本一致。且做了更進一步的優化,不管單次訪問的字節數是多少,整體的運行時間基本一致,Time(Munge8) == Time(Munge16) == Time(Munge32) == Time(Munge64)。

總結

本文的實驗僅能說明在魅族16x(高通驍龍sdm710平台)上:

  • 三種指令集上非對齊內存訪問所需開銷和對齊內存訪問的開銷基本一致。
  • armeabi和armeabi-v7a指令集上,當地址不對齊時,運行Munge64(非對齊地址單次訪問8 byte)會出現BUS_ADRALN的總線錯誤。
  • arm64-v8a在內存訪問上做了更進一步的優化,不管單次訪問的字節數是多少,整體的運行時間基本一致。


免責聲明!

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



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