1. :在c语言中什么意思
:在C语言中,通常有两种用法:
1,作为三元运算符的一部分,三元运算符表达式形式如下:
表达式?表达式1:表达式2
其计算顺序为先计算表达式的值,如果它的值为真,则整个表达式返回表达式1的值,否则返回表达式2的值。可以嵌套使用。比如:
inta=1,b=2;
returna>b?true:false;//a>b为假,所以返回false
2,作为结构体的位域。结构体的位域,属于有点难度的知识点,它的目的是用来节省存储空间。基本的用法如下:
struct 位域结构名
{
位域列表
};
其中,位域列表形式为:
类型说明符 位域名:位域长度
比如:
struct_data
{
inta:8;//位域占8bit
intb:2;//位域占2bit
intc:6;//位域占6bit
}T_data;
关于位域,有一些使用的规则,可以去找找相关资料来看看。
2. 如何高效的访问内存
影响内存访问速度的因素主要有:
1.内存带宽:每秒读写内存的数据量,由硬件配置决定。
2.CACHE高速缓冲:CPU与内存之间的缓冲器,当命中率比较高时能大大提供内存平均访问速度。
3.TLB转换旁视缓冲:系统虚拟地址向物理地址转换的高速查表机制,转换速度比普通转换机制要快。
我们能够优化的只有第2点和第3点。由于CACHE的小容量与SMP的同步竞争,如何最大限度的利用高速缓冲就是我们的明确优化突破口(以常用的数据结构体为例):
1.压缩结构体大小:针对CACHE的小容量。
2.对结构体进行对齐:针对内存地址读写特性与SMP上CACHE的同步竞争。
3.申请地址连续的内存空间:针对TLB的小容量和CACHE命中。
4.其它优化:综合考虑多种因素
具体优化方法
1.压缩结构体大小
系统CACHE是有限的,并且容量很小,充分压缩结构体大小,使得CACHE能缓存更多的被访问数据,无非是提高内存平均访问速度的有效方法之一。
压缩结构体大小除了需要我们对应用逻辑做好更合理的设计,尽量去除不必要的字段,还有一些额外针对结构体本身的压缩方法。
1.1.对结构体字段进行合理的排列
由于结构体自身对齐的特性,具有同样字段的结构体,不同的字段排列顺序会产生不同大小的结构体。
大小:12字节
struct box_a
{
char a;
short b;
int c;
char d;
};
大小:8字节
struct box_b
{
char a;
char d;
short b;
int c;
};
1.2.利用位域
实际中,有些结构体字段并不需要那么大的存储空间,比如表示真假标记的flag字段只取两个值之一,0或1,此时用1个bit位即可,如果使用int类型的单一字段就大大的浪费了空间。
示例:tcp.h
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your defines"
#endif
__be16 window;
__sum16 check;
__be16 urg_ptr;
};
1.3.利用union
union结构体也是压缩结构体大小的方法之一,它允许我们在某些情况下能对结构体的多个字段进行合并或把小字节字段存放到大字节字段内。
示例:skbuff.h
struct sk_buff {
…
union {
__wsum csum;
struct {
__u16 csum_start;
__u16 csum_offset;
};
};
…
};
2.对结构体进行对齐
对结构体进行对齐有两层意思,一是指对较小结构体进行机器字对齐,二是指对较大结构体进行CACHE LINE对齐。
2.1.对较小结构体进行机器字对齐
我们知道,对于现代计算机硬件来说,内存只能通过特定的对齐地址(比如按照机器字)进行访问。举个例子来说,比如在64位的机器上,不管我们是要读取第0个字节还是要读取第1个字节,在硬件上传输的信号都是一样的。因为它都会把地址0到地址7,这8个字节全部读到CPU,只是当我们是需要读取第0个字节时,丢掉后面7个字节,当我们是需要读取第1个字节,丢掉第1个和后面6个字节。
当我们要读取的字节刚好落在两个机器字内时,就出现两次访问内存的情况,同时通过一些逻辑计算才能得到最终的结果。
因此,为了更好的提升性能,我们须尽量将结构体做到机器字(或倍数)对齐,而结构体中一些频繁访问的字段也尽量安排在机器字对齐的位置。
大小:12字节
struct box_c
{
char a;
char d;
short b;
int c;
int e;
};
大小:16字节
struct box_d
{
char a;
char d;
short b;
int c;
int e;
char padding[4];
};
上面表格右边的box_d结构体,通过增加一个填充字段padding将结构体大小增加到16字节,从而与机器字倍数对齐,这在我们申请连续的box_d结构体数组时,仍能保证数组内的每一个结构体都与机器字倍数对齐。
通过填充字段padding使得结构体大小与机器字倍数对齐是一种常见的做法,在Linux内核源码里随处可见。
2.2.对较大结构体进行CACHE LINE对齐
我们知道,CACHE与内存交换的最小单位为CACHE LINE,一个CACHE LINE大小以64字节为例。当我们的结构体大小没有与64字节对齐时,一个结构体可能就要占用比原本需要更多的CACHE LINE。比如,把一个内存中没有64字节长的结构体缓存到CACHE时,即使该结构体本身长度或许没有还没有64字节,但由于其前后搭占在两条CACHE LINE上,那么对其进行淘汰时就会淘汰出去两条CACHE LINE。
这还不是最严重的问题,非CACHE LINE对齐结构体在SMP机器上容易引发名为错误共享的CACHE问题。比如,结构体T1和T2都没做CACHE LINE对齐,如果它们(T1后半部和T2前半部)在SMP机器上合占了同一条CACHE,如果CPU 0对结构体T1后半部做了修改则将导致CPU 1的CACHE LINE 1失效,同样,如果CPU 1对结构体T2前半部做了修改则也将导致CPU 0的CACHE LINE 1失效。如果CPU 0和CPU 1反复做相应的修改则导致的不良结果显而易见。本来逻辑上没有共享的结构体T1和T2,实际上却共享了CACHE LINE 1,这就是所谓的错误共享。
Linux源码里提供了利用GCC的__attribute__扩展属性定义的宏来做这种对齐处理,在文件/linux-2.6.xx/include/linux/cache.h内可以找到多个相类似的宏,比如:
点击(此处)折叠或打开
#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES)))
该宏可以用来修饰结构体字段,作用是强制该字段地址与CACHE LINE映射起始地址对齐。
看/linux-2.6.xx/drivers/net/e100.c内结构体nic的实现,三个____cacheline_aligned修饰字段,表示强制这些字段与CACHE LINE映射起始地址对齐。
点击(此处)折叠或打开
struct nic {
/* Begin: frequently used values: keep adjacent for cache effect */
u32 msg_enable ____cacheline_aligned;
/* 4字节空洞 */
struct net_device *netdev;
struct pci_dev *pdev;
/* 40字节空洞 */
struct rx *rxs ____cacheline_aligned;
struct rx *rx_to_use;
struct rx *rx_to_clean;
struct rfd blank_rfd;
enum ru_state ru_running;
/* 20字节空洞 */
spinlock_t cb_lock ____cacheline_aligned;
spinlock_t cmd_lock;
struct csr __iomem *csr;
enum scb_cmd_lo cuc_cmd;
unsigned int cbs_avail;
struct napi_struct napi;
…
}
回到前面的问题,如果我们对结构体T2的第一个字段加上____cacheline_aligned修饰,则该错误共享即可解决。
2.3.只读字段和读写字段隔离对齐
只读字段和读写字段隔离对齐的目的就是为了尽量保证那些只读字段和读写字段分别集中在CACHE的不同CACHE LINE中。由于只读字段几乎不需要进行更新,因而能在CACHE中得以稳定的缓存,减少由于混合有读写字段导致的对应CACHE LINE的频繁失效问题,以便提高效率;而读写字段相对集中在一起,这样也能保证当程序读写结构体时,污染的CACHE LINE条数也就相对的较少。
点击(此处)折叠或打开
typedef struct {
/* ro data */
size_t block_count; // number of total blocks
size_t meta_block_size; // sizeof per skb meta block
size_t data_block_size; // sizeof per skb data block
u8 *meta_base_addr; // base address of skb meta buffer
u8 *data_base_addr; // base address of skb data buffer
/* rw data */
size_t current_index ____cacheline_aligned; // index
} bc_buff, * bc_buff_t;
3.申请地址连续的内存空间
随着地址空间由32位转到64位,页内存管理的目录分级也越来越多,4级的目录地址转换也是一笔不小是开销。硬件产商为我们提供了TLB缓冲,加速虚拟地址到物理地址的换算。但是,毕竟TLB是有限,对地址连续的内存空间进行访问时,TLB能得到更多的命中,同时CACHE高速缓冲命中的几率也更大。
两段代码,实现同一功能,但第一种方法在实际使用中,内存读写效率就会相对较好,特别是在申请的内存很大时(未考虑malloc异常):
点击(此处)折叠或打开
方法一:
#define MAX 100
int i;
char *p;
struct box_d *box[MAX];
p = (char *)malloc(sizeof(struct box_d) * MAX);
for (i = 0; i < MAX; i ++)
{
box[i] = (struct box_d *)(p + sizeof(struct box_d) * i);
}
方法二:
#define MAX 100
int i;
struct box_d *box[MAX];
for (i = 0; i < MAX; i ++)
{
box[i] = (struct box_d *)malloc(sizeof(struct box_d));
}
另外,如果我们使用更大页面(比如2M或1G)的分页机制,同样能够提升性能;因为相比于原本每页4K大小的分页机制,应用程序申请同样大小的内存,大页面分页机制需要的页面数目更少,从而占用的TLB项目也更少,减少虚拟地址到物理地址的转换次数的同时,提高TLB的命中率,缩短每次转换所需要的时间。因为大多数操作系统在分配内存时候都需要按页对齐,所以大页面分页机制的缺点就是内存浪费相对比较严重。只有在物理内存足够充足的情况下,大页面分页机制才能够体现出优势。
4.其它优化
4.1.预读指令读内存
提前预取内存中数据到CACHE内,提高CACHE的命中率,加速内存读取速度,这是设计预读指令的主要目的。如果当前运算复杂度比较高,那么预取和运算就可同步进行,从而消除下一步内存访问的时延。相应的预读汇编指令有prefetch0、prefetch1、prefetch2、 prefetchnta。
预取指令只是给CPU一个提示,所以它可被CPU忽略,而且就算预取一段错误的地址也不会导致CPU异常。一般使用prefetchnta预取指令,因为它不会污染CACHE,它把每次取得的数据都存放到L2 CACHE的第一条CACHE LINE,而另外几条指令会替换CACHE中最近最少使用的CACHE LINE。
4.2.非暂时移动指令写内存
我们知道为了保证CACHE与内存之间的数据一致性,CPU对CACHE的写操作主要有两种方式同步到内存,写透式(Write Through)和写回式(Write-back)。不管哪种同步方式都是要消耗性能的,而在某些情况下,写CACHE是不必要的:
有哪些情况不需要写CACHE呢?比如做数据拷贝(高效memcpy函数实现)时,或者我们已经知道写的数据在最近一段时间内(或者永远)都不会再使用了,那么此时就可以不用写CACHE,让对应的CACHE LINE自动失效,以便缓存其它数据。这在某些特殊场景非常有用,相应的汇编指令有movntq、movntsd、movntss、movntps、movntpd、movntdq、movntdqa。
完整的利用预读指令和非暂时移动指令实现的高速内存拷贝函数:
点击(此处)折叠或打开
void X_aligned_memcpy_sse2(void* dest, const void* src, const unsigned long size_t)
{
__asm
{
mov esi, src; //src pointer
mov edi, dest; //dest pointer
mov ebx, size_t; //ebx is our counter
shr ebx, 7; //divide by 128 (8 * 128bit registers)
loop_:
prefetchnta 128[ESI]; //SSE2 prefetch
prefetchnta 160[ESI];
prefetchnta 192[ESI];
prefetchnta 224[ESI];
movdqa xmm0, 0[ESI]; //move data from src to registers
movdqa xmm1, 16[ESI];
movdqa xmm2, 32[ESI];
movdqa xmm3, 48[ESI];
movdqa xmm4, 64[ESI];
movdqa xmm5, 80[ESI];
movdqa xmm6, 96[ESI];
movdqa xmm7, 112[ESI];
movntdq 0[EDI], xmm0; //move data from registers to dest
movntdq 16[EDI], xmm1;
movntdq 32[EDI], xmm2;
movntdq 48[EDI], xmm3;
movntdq 64[EDI], xmm4;
movntdq 80[EDI], xmm5;
movntdq 96[EDI], xmm6;
movntdq 112[EDI], xmm7;
add esi, 128;
add edi, 128;
dec ebx;
jnz loop_; //loop please
loop__end:
}
}
总结
要高效的访问内存,必须充分利用系统CACHE的缓存功能,因为就目前来说,CACHE的访问速度比内存快太多了。具体优化方法有:
1.用设计上压缩结构体大小。
2.结构体尽量做到机器字(倍数)对齐。
3.结构体中频繁访问的字段尽量放在机器字对齐的位置。
4.频繁读写的多个结构体变量尽量同时申请,使得它们尽可能的分布在较小的线性空间范围内,这样可利用TLB缓冲。
5.当结构体比较大时,对结构体字段进行初始化或设置值时最好从第一个字段依次往后进行,这样可保证对内存的访问是顺序进行。
6.额外的优化可以采用非暂时移动指令(如movntdq)与预读指令(如prefetchnta)。
7.特殊情况可考虑利用多媒体指令SSE2、SSE4等。
当然,上面某些步骤之间存在冲突,比如压缩结构体和结构体对齐,这就需要实际综合考虑。
3. C语言结构体位域问题
不是的,结构体变量只是整个结构体存储的首地址,每个位域没有具体规定大小,所以整个结构体也没有规定大小,要看你定义的位域的类型。例如8个int型位域和8个double型位域存储空间是不一样的
4. C语言中位域大小与宽度该怎么算
1 8;
分析第一个: 在结构体里 他使用了位域 然后 a使用了2位 b使用了3位 c使用了3位 这里刚好用掉了8位 一共是一个字节 然后在对结构体分析 结构体里只有unsigned char类型 这个类型占了一个字节 然后 然后结构体的大小必须是结构体里最长的类型的整数倍 这里面就是1的整数倍 然后结合上面用了1字节 所以结果是1
分析第二个: 2 3 3一样的和上面, 然后里面多一个unsiged int 这个东西在里面占用4个字节 那么结构体的大小就必须是4的倍数 然后里面存取的结果就是 a 2位 b 3位 c 3位 然后这是一个字节 然后与unsiged int对其 空出3个字节补0; 然后放一个d; 然后d前四位有数 后面补0;
5. 解释下位域,为什么要用位域,位域的好处
位域是指信息在存储时,并不需要占用一个完整的字节,
而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1
两种状态,
用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为"位域"或"位段"。所谓"位域"是把一个字节中的二进位划分为几
个不同的区域,
并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。
这样就可以把几个不同的对象用一个字节的二进制位域来表示。
使用位域的好处是:
1.有些信息在存储时,并不需要占用一个完整的字节,
而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1
两种状态,
用一位二进位即可。这样节省存储空间,而且处理简便。
这样就可以把几个不同的对象用一个字节的二进制位域来表示。
2.可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间。