[工作积累] bitfield
ISO/IEC 14882:2003: 9.6 Bit-fields [class.bit]
- A member-declarator of the form identifieropt : constant-expression specifies a bit-field; its length is set off from the bit-field name by a colon. The bit-field attribute is not part of the type of the class member. The constant-expression shall be an integral constant-expression with a value greater than or equal to zero. The constant-expression may be larger than the number of bits in the object representation (3.9) of the bit-field’s type; in such cases the extra bits are used as padding bits and do not participate in the value representation (3.9) of the bit-field. Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit. [Note: bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. ]
- A declaration for a bit-field that omits the identifier declares an unnamed bit-field. Unnamed bit-fields are not members and cannot be initialized. [Note: an unnamed bit-field is useful for padding to conform to externally-imposed layouts. ] As a special case, an unnamed bit-field with a width of zero specifies alignment of the next bit-field at an allocation unit boundary. Only when declaring an unnamed bit-field may the constant-expression be a value equal to zero.
- A bit-field shall not be a static member. A bit-field shall have integral or enumeration type (3.9.1). It is implementation-defined whether a plain (neither explicitly signed nor unsigned) char, short, int or long bit-field is signed or unsigned. A bool value can successfully be stored in a bit-field of any nonzero size. The address-of operator & shall not be applied to a bit-field, so there are no pointers to bit-fields. A non-const reference shall not be bound to a bit-field (8.5.3). [Note: if the initializer for a reference of type const T& is an lvalue that refers to a bit-field, the reference is bound to a temporary initialized to hold the value of the bit-field; the reference is not bound to the bit-field directly. See 8.5.3. ]
- If the value true or false is stored into a bit-field of type bool of any size (including a one bit bitfield), the original bool value and the value of the bit-field shall compare equal. If the value of an enumerator is stored into a bit-field of the same enumeration type and the number of bits in the bit-field is large enough to hold all the values of that enumeration type, the original enumerator value and the value of the bit-field shall compare equal.
最近在64bit IOS上遇到一个问题, 发现bit field的数据不对. 这个bit field是一个data struct, 在host(win32)上生成. 运行时候发现IOS取的值不对.
比如
//IOS 32 struct { //allocation unit 1 int a : 4; int b : 8;//12bit int c : 16; //28bit int d : 8; //4 bit allocation uint 1, 4 bit allocation unit 2 int e : 16; //20 bit in allocation unit 2 }; //IOS 64 struct { //allocation unit 1 int a : 4; int b : 8;//12bit int c : 16; //28bit //allocation unit 2 int d : 8; //8 bit int e : 16; //24 bit };
在IOS的32位上, d会横跨两个allocation uint, 而在IOS的64位上, d直接从第二个allocation unit上从新开始. clang在编译32位跟64位时的处理方式不一样,不知道是不是bug. 就算它是clang的bug, 但仍然是遵循标准的.因为C++标准说了可以这样.
根据C++标准, 这个是implementation defined behavior, 编译器可以选择横跨也可以不横跨, 都可以.
在host(win32)上的打包程序, MSVC也是横跨方式, 与clang处理32 bit IOS的方式一致. 所以(凑巧)host生成的数据可以在IOS32上面使用, 可能大多数编译器都以横跨方式处理bitfields吧.
解决方法是:将a,b,c对齐到32bit的整数, 这样强制d从新的allocation unit开始, 没有横跨的问题, 对于IOS32位和IOS64位都有效.
//shared disk data //IOS 32 struct { //allocation unit 1 int a : 4; //4bit int b : 8; //12bit int c : 20; //32bit //allocation unit 2 int d : 8; //8 bit int e : 16; //24 bit }; //IOS 64 struct { //allocation unit 1 int a : 4; //4bit int b : 8; //12bit int c : 20; //32bit //allocation unit 2 int d : 8; //8 bit int e : 16; //24 bit };
另外, IOS 64bit的额外参考: 对齐等等其他因素
bitfield用在game data里面, 如果多平台共享的同一份数据话, 就有类似上面的的问题. 甚至顺序也有可能不一样(left-to-right/right-to-left), 所以blade里面没有用过. 而公司的代码用的地方非常多.
公司的代码虽然是跨平台的, 但是之前是每个平台都有不同的数据, 现在IOS 32/64要共享同一份数据, 才会有上面的问题. 这个是历史的原因.毕竟积累了近20年的代码. 注重代码和技术积累, 这样的公司才靠谱.
另外公司有自己的编译器, 最好的方式是在编译器上直接解决bitfield的straddle和left-to-right/right-to-left的问题. 这样上面的代码就不用关心这些琐碎的细节了.
个人的观点是每个平台使用通用的数据最好.
以下内容摘自我的另一博客(http://hi.baidu.com/crazii_chn/item/62705798f8a76bd91b49dfd8):
一般来说, 需要根据每一种目标平台, 各生成一份对应的数据.
这样做的好处是每个平台的数据可以根据目标特性做调整.比如移动平台的场景规模/质量和数据量一般都相对要小.
由于每种CPU的数据处理特性(cache line, alignment, SIMD指令集)可能都不一样,所以需要做特别的处理.对于数据的格式,有两种做法, 第一是每个平台的数据包格式不一样, 第二是种是使用统一的格式, 也就是说尽管每个目标平台都有不同的一个数据包(因目标规模而定), 但理论上某个平台可以正常加载其他所有平台的数据包.
而第一种则不同, 各个平台的数据格式不一样, 比如一个struct的成员对齐不同, 那么序列化后的数据会有大小和偏移量的差别.第一种方式不用特别考虑数据对齐等等问题, 但是要生成和维护不同目标平台下的数据包和工具, 虽然维护成本相对较低,但是如果工具很多的时候也很繁琐.
第二种方式需要在代码中考虑对齐等等因素, 保证一个数据包可以被所有的平台加载, 这么做可能也很繁琐,需要特别小心,并经过完整的数据测试.这里有一个android下x86和arm的结构对齐的处理 (4.1 Forced Memory Alignment):
http://software.intel.com/en-us/articles/android-application-development-and-optimization-on-the-intel-atom-platform
参考http://software.intel.com/en-us/blogs/2011/08/18/understanding-x86-vs-arm-memory-alignment-on-android/
比如上面第一个链接中使用的"-malign-double" GCC参数, 强制8字节的数据对齐的8字节边界.保证同一份数据可以被不同的CPU正常加载. 第二个链接使用的是__attribute__((aligned(n))), 来强制数据成员的对齐.我所在的公司使用的是第一种方式. 第一种方式的问题在于同一个OS也有不同的target CPU(platform), 这样会出现对于android平台, x86有一份数据, 而arm有另外一份数据. 所以个人倾向于第二种方式, 除了上文链接中的方式以外, 也可以强制不读写struct, 只读写原子数据, 把一个struct的成员逐个单独读写.
如何保证数据能够通用呢? 严格按照C/C++标准来做. 如果是implementation defined或者unspecified的定义, 那么就规避, 不要使用, 比如上面的那个例子. undefined behavior更不能用. 这样在host编译出的data-tools, 处理数据的方式与target上的executables处理数据的方式一致, 能够保证host上打包生成的同一份数据, 在所有平台上都可以用.