• 对内存对齐的深一步理解


      接触内存对齐这个概念,也有三四年了。不过由于我工作后一直做游戏服务器,都是在x86架构的机子上写代码,也没怎么注意内存对齐。使用最多的估计也就是面试时经常问结构体大小。最近在写自己服务器框架的二进流读写模块时,整理了下这方面的内容。本方不会涉及基本概念。

      内存对齐只是指数据存储在内存时的起始地址是否是某个值的整数倍。如果只是放在内存中,是否对齐本身并没有什么问题。问题是读取、写入的时候。访问一个不对齐的数据(unaligned memory access)可能会导致程序运行效率慢,结果出错,甚至是程序当掉。那这些情况是怎么出现的呢?

      我们都知道,程序最终都是以CPU指令来运行的。参考:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html,我们知道ARM CPU有下面几条指令:

    LDRB/STRB          - address must be byte aligned
    LDRH/STRH          - address must be 2-byte aligned 
    LDR/STR            - address must be 4-byte aligned
    

    LDRB/STRB字节加载、存储指令

    LDRH/STRH半字(即2byte,不是半字节)加载、存储指令

    LDR/STR 字加载、存储指令

    也就是说,当我们从内存中存取数据时,要调用上面的指令。而这些指令在设计时,较老的CPU由于考虑了硬件、效率等等问题,要求访问的内存必须是对齐的。现在假如我声明了一个内存缓冲区char *buffer[1024],系统给它分配的地址是0x00001000,可以看到,这个地址都是符合1、2、4字节对齐的。接着我从网络接收了一段数据,放到这个缓冲区里。现在要从缓冲区里依次取出char、int两个类型的数据:

    char ch = *buffer;
    int i = *reinterpret_cast<int *>(buffer+1);
    

    运行ch = *buffer时,由于char类型的大小是1字节,CPU将调用LDRB指令,这时将检测buffer是否按1byte对齐。这里当然是对齐的,所以指令运行正常。

    运行i = *reinterpret_cast<int *>(buffer+1)时,由于int类型大小是4字节,CPU将调用LDR指令,这时检测buffer+1(0x00001001)是否按4byte对齐,结果发现不对齐,CPU将报错,程序中止。

    而安全的做法是这样的:

    memcpy( &ch,buffer,1 );
    memcpy( &i,buffer+1,4 );
    

    你可能会问,使用memcpy,buffer+1的地址也是不对齐的,为什么就安全了呢?就像我上面所说的,数据在内存中存放时,是否对齐并不重要,重要的是你怎样去访问它。memcpy的实现本身并不简单(你在源码里看到的通过while每次拷贝一个char的只是一个例子,并不是真实的memcpy),它考虑了是否对齐。当检测到内存是对齐时,memcpy调用合适的指令(比较这里拷贝一个int,就调用LDR),一次拷贝多个字节,以提高效率。当检测到不对齐时,先调用LDRB遂个字节拷贝,直到对齐部分后再调用合适的指令拷贝。因此,在上面的例子中,它是先调用LDRB的,因为LDRB是按1byte对齐(所有的内存都按这个对齐),所以不会触发报错。但效率就要慢一点了,毕竟要拷贝几次。

      内存对齐本身对程序员来说是透明的,即程序员该取变量就取变量,该存就存,编译程序时编译器会把变量按本身的平台进行对齐。况且现在的CPU都很高级,别说服务器,台式机的CPU,ARM 7以上应该也支持内存不对齐访问了。但如果你要写一个内存池(boost的ordered_pool有对齐的例子),或者使用了reinterpret_cast这种对内存直接进行操作的函数,这方面还是要注意一下,即使CPU支持,效率也会受到影响。

      我在很多项目中,发现这样的写法:

    #pragma pack(push,1)
    struct NetPack
    {
        //...
    };
    #pragma pack(pop)
    

    这是强制把这个结构体按1byte对齐,当有网络数据过来,直接memcpy整个结构体就可以。有趣的时,我在内核文档里发现这么一段话:https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt

    Another point worth mentioning is the use of __attribute__((packed)) on a
    structure type. This GCC-specific attribute tells the compiler never to
    insert any padding within structures, useful when you want to use a C struct
    to represent some data that comes in a fixed arrangement 'off the wire'.
    
    You might be inclined to believe that usage of this attribute can easily
    lead to unaligned accesses when accessing fields that do not satisfy
    architectural alignment requirements. However, again, the compiler is aware
    of the alignment constraints and will generate extra instructions to perform
    the memory access in a way that does not cause unaligned access. Of course,
    the extra instructions obviously cause a loss in performance compared to the
    non-packed case, so the packed attribute should only be used when avoiding
    structure padding is of importance.

    当我们把变量强制按1byte对齐时,编译器不会在结构体中加入任何内容来使得这个结构体符合内存对齐,而是产生一些额外的指令来让他满足当前平台的内存对齐,当然,效率还是受影响的。

  • 相关阅读:
    有些文件不需要配置,只需要放到resources下面
    RAFT算法
    HBase,region以及HFile概念
    GitHub搭建个人网站续
    sublime text 2 安装emmet插件
    前端收录
    Zepto
    将复杂form表单序列化serialize-object.js
    PHP 清除HTML代码、空格、回车换行符的函数
    Yii2 选择布局的方式
  • 原文地址:https://www.cnblogs.com/coding-my-life/p/5374562.html
Copyright © 2020-2023  润新知