🚙

💨 💨 💨

×

  • Categories

  • Archives

  • Tags

  • About

关系型数据库与NoSQL的爱恨情仇

Posted on 05-20-2015 | In DB

NoSQL因关系数据库的不足而生

随着互联网的不断发展,各种类型的应用层出不穷,所以导致在这个云计算的时代,

对技术提出了更多的需求,主要体现在下面这四个方面:

  • 低延迟的读写速度:应用快速地反应能极大地提升用户的满意度;
  • 支撑海量的数据和流量:对于搜索这样大型应用而言,需要利用PB级别的数据和能应对百万级的流量;
  • 大规模集群的管理:系统管理员希望分布式应用能更简单的部署和管理;

庞大运营成本的考量:IT经理们希望在硬件成本、软件成本和人力成本能够有大幅度地降低;

目前世界上主流的存储系统大部分还是采用了关系型数据库,其主要有一下优点:

  • 事务处理—保持数据的一致性;
  • 由于以标准化为前提,数据更新的开销很小(相同的字段基本上只有一处);
  • 可以进行Join等复杂查询。

虽然关系型数据库已经在业界的数据存储方面占据不可动摇的地位,但是由于其天生的几个限制,

使其很难满足上面这几个需求:

  • 扩展困难:由于存在类似Join这样多表查询机制,使得数据库在扩展方面很艰难;
  • 读写慢:这种情况主要发生在数据量达到一定规模时由于关系型数据库的系统逻辑非常复杂,使得其非常容易发生死锁等的并发问题,所以导致其读写速度下滑非常严重;
  • 成本高:企业级数据库的License价格很惊人,并且随着系统的规模,而不断上升;
  • 有限的支撑容量:现有关系型解决方案还无法支撑Google这样海量的数据存储;

业界为了解决上面提到的几个需求,推出了多款新类型的数据库,并且由于它们在设计上和传统的NoSQL数据库相比有很大的不同,
所以被统称为“NoSQL”系列数据库。

总的来说,在设计上,它们非常关注对数据高并发地读写和对海量数据的存储等,与关系型数据库相比,它们在架构和数据模型方量面做了“减法”,

而在扩展和并发等方面做了“加法”。

现在主流的NoSQL数据库有MongoDB和Redis以及BigTable、Hbase、Cassandra、SimpleDB、CouchDB、等。

接下来,将关注NoSQL数据库到底存在哪些优缺点。

NoSQL的优缺点

在优势方面,主要体现在下面这三点:

  • 简单的扩展:典型例子是Cassandra,由于其架构是类似于经典的P2P,所以能通过轻松地添加新的节点来扩展这个集群;
  • 快速的读写:主要例子有redis,由于其逻辑简单,而且纯内存操作,使得其性能非常出色,单节点每秒可以处理超过10万次读写操作;
  • 低廉的成本:这是大多数分布式数据库共有的特点,因为主要都是开源软件,没有昂贵的License成本;

但瑕不掩瑜,NoSQL数据库还存在着很多的不足,常见主要有下面这几个:

  • 不提供对SQL的支持:如果不支持SQL这样的工业标准,将会对用户产生一定的学习和应用迁移成本;
  • 支持的特性不够丰富:现有产品所提供的功能都比较有限,大多数NoSQL数据库都不支持事务,也不像MS SQL Server和Oracle那样能提供各种附加功能,比如BI和报表等;
  • 现有产品的不够成熟:大多数产品都还处于初创期,和关系型数据库几十年的完善不可同日而语;

上面NoSQL产品的优缺点都是些比较共通的,在实际情况下,每个产品都会根据自己所遵从的数据模型和CAP理念而有所不同.

编译过程

Posted on 05-07-2015 | In Misc


通常我们使用gcc来生成可执行程序,命令为:gcc hello.c,默认生成可执行文件a.out



其实编译(包括链接)的命令:gcc hello.c 可分解为如下4个大的步骤:




    • 预处理(Preprocessing)
    • 编译(Compilation)
    • 汇编(Assembly)
    • 链接(Linking)




gcc_compilation


预处理




1.       预处理(Preproceessing)



预处理的过程主要处理包括以下过程:



  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
  • 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有注释 “//”和”/ /”.
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们


 



通常使用以下命令来进行预处理:



gcc -E hello.c -o hello.i



参数-E表示只进行预处理 或者也可以使用以下指令完成预处理过程



cpp hello.c > hello.i      /  cpp – The C Preprocessor  /



直接cat hello.i 你就可以看到预处理后的代码



 

编译


2.       编译(Compilation)



编译过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。



$gcc –S hello.i –o hello.s



或者



$ /usr/lib/gcc/i486-linux-gnu/4.4/cc1 hello.c



注:现在版本的GCC把预处理和编译两个步骤合成一个步骤,用cc1工具来完成。gcc其实是后台程序的一些包装,根据不同参数去调用其他的实际处理程序,比如:预编译编译程序cc1、汇编器as、连接器ld



可以看到编译后的汇编代码(hello.s)如下:

 .file   "hello.c"
.section .rodata
.LC0:
.string "Hello, world."
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits


 

汇编


3.       汇编(Assembly)



汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。



$ gcc –c hello.c –o hello.o



或者



$ as hello.s –o hello.co



由于hello.o的内容为机器码,不能以普通文本形式的查看(vi 打开看到的是乱码)。



 

链接

4.       链接(Linking)



通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。



ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o (省略了文件的路径名)。



 

编译器和链接器具体做了什么


helloworld的大体编译和链接过程就是这样了,那么编译器和链接器到底做了什么呢?



 



编译过程可分为6步:

  • 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
  • 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
  • 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
  • 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
  • 目标代码生成:代码生成器(Code Generator).
  • 目标代码优化:目标代码优化器(Target Code Optimizer)。


 



链接的主要内容是把各个模块之间相互引用的部分处理好,使得各个模块之间能够正确地衔接。



链接的主要过程包括:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。

链接分为静态链接和动态链接

  1. 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。

  2. 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。


静态链接的大致过程如下图所示:

static_linking

关于Valgrind所报的4种内存丢失

Posted on 05-02-2015 | In Misc

官方解释及分析

摘自http://valgrind.org/docs/manual/faq.html#faq.deflost

5.2.With Memcheck’s memory leak detector, what’s the difference between “definitely lost”, “indirectly lost”, “possibly lost”, “still reachable”, and “suppressed”?

The details are in the Memcheck section of the user manual.
In short:

  • “definitely lost” means your program is leaking memory – fix those leaks!

  • “indirectly lost” means your program is leaking memory in a pointer-based structure. (E.g. if the root node of a binary tree is “definitely lost”, all the children will be “indirectly lost”.) If you fix the “definitely lost” leaks, the “indirectly lost” leaks should go away.

  • “possibly lost” means your program is leaking memory, unless you’re doing unusual things with pointers that could cause them to point into the middle of an allocated block; see the user manual for some possible causes. Use –show-possibly-lost=no if you don’t want to see these reports.

  • “still reachable” means your program is probably ok – it didn’t free some memory it could have. This is quite common and often reasonable. Don’t use –show-reachable=yes if you don’t want to see these reports.

  • “suppressed” means that a leak error has been suppressed. There are some suppressions in the default suppression files. You can ignore suppressed errors.

分析

  • “definitely lost”:确认丢失。程序中存在内存泄露,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。

  • “indirectly lost”:间接丢失。当使用了含有指针成员的类或结构时可能会报这个错误。这类错误无需直接修复,他们总是与”definitely lost”一起出现,只要修复”definitely lost”即可。例子可参考我的例程。

  • “possibly lost”:可能丢失。大多数情况下应视为与”definitely lost”一样需要尽快修复,除非你的程序让一个指针指向一块动态分配的内存(但不是这块内存起始地址),然后通过运算得到这块内存起始地址,再释放它。例子可参考我的例程。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。

  • “still reachable”:可以访问,未丢失但也未释放。如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。

  • “suppressed”:已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。这类错误我没能用例程触发,看官方的解释也不太清楚是操作系统处理的还是valgrind,也没有遇到过。所以无视他吧~

代码示例

#include <stdio.h>
#include <stdlib.h>

void *g_p1;
int *g_p2;
int ** fun1(void)
{
//付给了局部变量, 函数结束而不释放,为肯定丢失.
//把函数尾部语句return p; 改为return 0;更能说明这个问题.
int **p=(int **)malloc(16);
g_p1=malloc(20); //付给了全局变量, 内存可以访问
g_p2=(int*)malloc(30);
g_p2++; //付给了全局变量, 内存可以访问,但是指针被移动过,为可能丢失
p[1]=(int *)malloc(40); //如果p丢失了,则p[1]为间接丢失.
return p;
}
int main(int argc, char *argv[])
{

int **p=fun1();
// free(g_p1); //如果不free, 将会有 still reachable 内存泄露
// free(--g_p2);//如果不free, 将会有 possibly lost 内存泄露
// free(p[1]); //如果不free, 将会有 indirectly lost 内存泄露
// free(p); //如果不free, 将会有 definitely lost内存泄露
return 0;
}

执行编译命令g++ val_test.cpp -o v, 然后

当执行valgrind ./v 命令之后的简易内存错误报告 :

==4765== Memcheck, a memory error detector
==4765== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4765== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==4765== Command: ./v
==4765== 
==4765== 
==4765== HEAP SUMMARY:
==4765==     in use at exit: 106 bytes in 4 blocks
==4765==   total heap usage: 4 allocs, 0 frees, 106 bytes allocated
==4765== 
==4765== LEAK SUMMARY:
==4765==    definitely lost: 16 bytes in 1 blocks
==4765==    indirectly lost: 40 bytes in 1 blocks
==4765==      possibly lost: 30 bytes in 1 blocks
==4765==    still reachable: 20 bytes in 1 blocks
==4765==         suppressed: 0 bytes in 0 blocks
==4765== Rerun with --leak-check=full to see details of leaked memory
==4765== 
==4765== For counts of detected and suppressed errors, rerun with: -v
==4765== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
b@b-VirtualBox:~/tc/valgrind_test$ valgrind --leak-check=full
valgrind: no program specified
valgrind: Use --help for more information.

当执行valgrind --leak-check=full ./v 命令之后的详细内存错误报告 :

==4767== Memcheck, a memory error detector
==4767== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4767== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==4767== Command: ./v
==4767== 
==4767== 
==4767== HEAP SUMMARY:
==4767==     in use at exit: 106 bytes in 4 blocks
==4767==   total heap usage: 4 allocs, 0 frees, 106 bytes allocated
==4767== 
==4767== 30 bytes in 1 blocks are possibly lost in loss record 2 of 4
==4767==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4767==    by 0x40055E: fun1() (val_test.cpp:12)
==4767==    by 0x4005AB: main (val_test.cpp:20)
==4767== 
==4767== 56 (16 direct, 40 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4
==4767==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4767==    by 0x40053F: fun1() (val_test.cpp:10)
==4767==    by 0x4005AB: main (val_test.cpp:20)
==4767== 
==4767== LEAK SUMMARY:
==4767==    definitely lost: 16 bytes in 1 blocks
==4767==    indirectly lost: 40 bytes in 1 blocks
==4767==      possibly lost: 30 bytes in 1 blocks
==4767==    still reachable: 20 bytes in 1 blocks
==4767==         suppressed: 0 bytes in 0 blocks
==4767== Reachable blocks (those to which a pointer was found) are not shown.
==4767== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==4767== 
==4767== For counts of detected and suppressed errors, rerun with: -v
==4767== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

总结

  • 由局部变量指向的内存,如果不释放为肯定丢失,
  • 由此指针而引起的后续内存泄露,为间接丢失.
  • 由全局变量指向的内存如果不被释放,为still reachable,
  • 如果该变量改动过, 为可能丢失.

是啊,局部变量是栈变量,如果你不能把这个栈变量处理好,出了这个函数,指针地址就丢失了,这时那是肯定丢失了.

如果你付给的地址是全局变量,倒是可以访问,叫still reachable

但是如果你这个全局变量的值改动过, 那只有你知道怎样正确访问这块内存,别人可能就访问不到了,这叫可能丢失.

由肯定丢失而引起的进一步的内存丢失为间接丢失.

解决内存泄漏的顺序

所以碰到问题你首先要解决什么问题?

肯定丢失,
然后是可能丢失,
然后间接丢失,
然后still reachable!!!

多线程开发的一些基本概念

Posted on 04-27-2015 | In Linux

竞态(race condition)

软件层面上,竞态是指多个线程或进程读写一个共享资源 (或共享设备) 时的输出结果依赖于线程或进程的先后执行顺序或者时间;
( 更权威的介绍可以看 wiki )

至于为什么会发生竞态呢?很简单,因为并发,并发使多线程,多进程环境变成可能。

竞态具体场景:假如我们有 2 个进程会对一个全局变量进行 ++ 操作,理想时,程序会这样执行:


Thread 1



Thread 2





Integer value









0



read value





←



0



increase value







0



write back





→



1





read value



←



1





increase value





1





write back



→



2


然而,由于并发的普遍存在,使得情况有时” 不受控制”(不如工程师预期那样工作),可能会变成这样:


Thread 1



Thread 2





Integer value









0



read value





←



0





read value



←



0



increase value







0





increase value





0



write back





→



1





write back



→



1


并发(concurrency)

并发 (concurrency) 指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源 (硬件资源和软件上的全局、静态变量) 的访问则容易导致竞态 (race conditions), 可能导致并发 (即竞态?) 的情况有:

  • SMP(Symmetric Multi-Processing),对称多处理结构。SMP 是一种紧耦合、共享存储的系统模型,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和存储器。

  • 中断. 中断可以打断正在执行的进程 (哪怕是在中断上下文),若中断处理程序对共享资源进程访问,则竞态也会发生.

  • 内核抢占.2.6 以后内核提供了内核可抢占特性,虽然是作为一个配置选项,但我们写程序时还是要考虑周全,故内核抢占也是作为伪并发的表现,也可能发生竞态;

临界区(critical section)

多个线程进程对共享资源进行访问在软件表现为一个程序片段,如何避免竞态的发生呢?

一个执行路径在对共享资源进行访问时禁止其他执行路径进行访问,当有一个执行路径(A)对共享资源进行访问时,如有其他执行路径想访问共享资源,须睡眠等待 A 执行路径退出。

那么这时这个程序片段就是临界区。那么具体如何来实现临界区呢?linux 内核提供了多种同步互斥机制.(如信号量,互斥量,自旋锁,RCU,原子操作等).

什么是RAII技术

我们在C++中经常使用new申请了内存空间,但是却也经常忘记delete回收申请的空间,容易造成内存溢出,于是RAII技术就诞生了,来解决这样的问题。

RAII(Resource Acquisition Is Initialization)机制是Bjarne Stroustrup首先提出的,是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 我们知道在函数内部的一些成员是放置在栈空间上的,当函数返回时,这些栈上的局部变量就会立即释放空间,于是Bjarne Stroustrup就想到确保能运行资源释放代码的地方就是在这个程序段(栈)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。RAII就利用了栈里面的变量的这一特点。

RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。

借此,我们实际上把管理一份资源的责任托管给了一个存放在栈空间上的局部对象。
这种做法有两大好处:

  • (1)不需要显式地释放资源。
  • (2)采用这种方式,对象所需的资源在其生命期内始终保持有效。

MySQL入门三之GroupBy

Posted on 04-17-2015 | In DB

SQL GROUP BY 实例

我们拥有下面这个 “Orders” 表:

O_Id OrderDate OrderPrice Customer
1 2008/12/29 1000 Bush
2 2008/11/23 1600 Carter
3 2008/10/05 700 Bush
4 2008/09/28 300 Bush
5 2008/08/06 2000 Adams
6 2008/07/21 100 Carter

现在,我们希望查找每个客户的总金额(总订单)。
我们想要使用 GROUP BY 语句对客户进行组合。
我们使用下列 SQL 语句:

SELECT Customer,SUM(OrderPrice) FROM Orders GROUP BY Customer

结果集类似这样:

Customer SUM(OrderPrice)
Bush 2000
Carter 1700
Adams 2000

很棒吧,对不对?
让我们看一下如果省略 GROUP BY 会出现什么情况:

SELECT Customer,SUM(OrderPrice) FROM Orders

结果集类似这样:

Customer SUM(OrderPrice)
Bush 5700
Carter 5700
Bush 5700
Bush 5700
Adams 5700
Carter 5700

上面的结果集不是我们需要的。
那么为什么不能使用上面这条 SELECT 语句呢?

解释如下:
上面的 SELECT 语句指定了两列(Customer 和 SUM(OrderPrice))。
“SUM(OrderPrice)” 返回一个单独的值(”OrderPrice” 列的总计),而 “Customer” 返回 6 个值(每个值对应 “Orders” 表中的每一行)。
因此,我们得不到正确的结果。不过,您已经看到了,GROUP BY 语句解决了这个问题。

对象模型之内存对齐基础

Posted on 04-12-2015 | In Misc

本文不讨论类的虚函数, 请参考 C++对象模型之虚函数讲解

内存对齐规则

首先我们明确内存对齐规则

我们设

A = #pragma pack()指定的数
B = 这个数据成员的自身长度
C = 结构(或联合)中最大数据成员长度

在解释内存对齐的作用前,先来看下内存对齐的规则:

1. 对于结构的各个成员,第一个成员位于偏移为0的位置,
以后每个数据成员的偏移量必须是 min( A,B ) 的倍数。

2. 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照 min( A, C) 进行。

问题

32位机器上, 下列代码的sizeof(a)的值是多少?

#pragma pack(2)

class A
{
int i;

union U
{

char buff[13];

int i;

}u;

void foo() { }

typedef char* (*f)(void*);

enum{ red, green, blue } color;
}a;
#pragma pack()

答案

答案是sizeof(a)的值为22.

  • void foo() { } ,typedef char* (f)(void);不占字节,
  • 枚举占4个字节,
  • union按最大的变量所占字节算,占14个字节,
  • int占4个字节,

4+14+4=22。

如果把#pragma pack(2)改为 #pragma pack(4), sizeof(a)的值就为 24。

解析

分为三部分来解析:

  • 枚举所占内存计算方法
  • #pragma pack用法
  • 共用体(union)所占内存计算方法

枚举所占内存计算方法

枚举变量,由枚举类型定义的变量。枚举变量的大小,即枚举类型所占内存的大小。

由于枚举变量的赋值,一次只能存放枚举结构中的某个常数。

所以枚举变量的大小,实质是常数所占内存空间的大小(常数为int类型,当前主流的编译器中一般是32位机器和64位机器中int型都是4个字节),枚举类型所占内存大小也是这样。

#pragma pack用法

#pragma pack(a)规定的对齐长度(a可选值为1,2,4,8,16),实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。
而 #pragma pack() 表示恢复默认的内存对齐(与#pragma pack(a)指令配对使用)

#pragma pack(4)

class TestB
{
public:

int aa; //第一个成员,放在[0,3]偏移的位置,

char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。

short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。

char c; //第四个,自身长为1,放在[8]的位置。

};
#pragma pack()

这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。

如果

#pragma pack(2)
class TestB
{
public:
int aa; //第一个成员,放在[0,3]偏移的位置,
char a; //第二个成员,自身长为1,#pragma pack(2),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
short b; //第三个成员,自身长2,#pragma pack(2),取2,按2字节对齐,所以放在偏移[6,7]的位置。
char c; //第四个,自身长为1,放在[8]的位置。
};
#pragma pack()

可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
所以 sizeof(TestB)是10。

现在去掉第一个成员变量为如下代码:

#pragma pack(4)
class TestC
{
public:
  char a;//第一个成员,放在[0]偏移的位置,
  short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
  char c;//第三个,自身长为1,放在[4]的位置。
};
#pragma pack()

整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6,所以sizeof(TestC)是6。

共用体(union)所占内存计算方法

共用体又名”联合体”, 英文名为union.

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的:

  • 联合体是一个结构;
  • 它的所有成员相对于基地址的偏移量都为0;
  • 此结构空间要大到足够容纳最”宽”的成员;
  • 其对齐方式要适合其中所有的成员;

下面解释这四条描述:

由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的。为了使得所有成员能够共享一段内存,因此该空间必须足够容纳这些成员中最宽的成员。对于这句“对齐方式要适合其中所有的成员”是指其必须符合所有成员的自身对齐方式。

下面举例说明:

union U
{
char s[9];
int n;
double d;
};

s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。

因此补充字节到16,这样就符合所有成员的自身对齐了。从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:

  • 大小足够容纳最宽的成员;
  • 大小能被其包含的所有基本数据类型的大小所整除。

若问题为#pragma pack(4)的情况

  • void foo() { } ,typedef char* (f)(void);不占字节,
  • 枚举占4个字节,
  • union按最大的变量buff[13]所占字节算为13, 在#pragma pack(2)的情况, 得补齐1个字节变为14才能被2整除, 而#pragma pack(4)的情况得补齐3个字节, 总占16个字节,才可以被4整除,
  • int占4个字节

所以#pragma pack(4)的情况, sizeof(A)为4+16+4=24。

练习

注意有陷阱, 32位环境下

# pragma pack(2)
class test_class
{
public:
static float i;

union test_union
{
int bb;
char aa[13];
short cc;
};

enum test_enum
{
monday,
tuesday,
sunday
};

virtual void testFunc() {}

char xmly;
};

# pragma pack()

int main()
{
cout << "sizeof(test_class) : " << sizeof(test_class) << endl;
return 0;
}

请问打印结果?

sizeof(test_class) : 6

为什么呢?
注意看共用体 test_union 和枚举 test_enum其实并没有声明变量, 如果写成

#include <iostream>

using namespace std;

# pragma pack(2)
class test_class
{
public:
static float i;

union test_union
{
int bb;
char aa[13];
short cc;
}uVar;

enum test_enum
{
monday,
tuesday,
sunday
}eVar;

virtual void testFunc() {}

char xmly;
};
# pragma pack()

enum enum_x
{
x1=5,
x2,
x3,
x4,
};
enum enum_x x=x3;

int main()
{
cout << "sizeof(test_class) : " << sizeof(test_class) << endl;

cout << "x : " << x << endl;

test_class::test_enum i;
i = test_class::monday;
cout << "i : " << i << endl;

test_class test_obj;
test_obj.eVar = test_class::sunday;
cout << test_obj.monday << endl;

cout << test_class::sunday << endl;
return 0;
}

打印结果就为

sizeof(test_class) : 24
x : 7
i : 0
0
2

GCC的分支预测优化__builtin_expect

Posted on 04-11-2015 | In Linux

1. 为什么需要分支预测优化

将流水线引入cpu,可以提高cpu的效率。更简单的说,让cpu可以预先取出下一条指令,可以提供cpu的效率。如下图所示:

取指令 执行指令 输出结果
取指令 执行

可见,cpu流水钱可以减少cpu等待取指令的耗时,从而提高cpu的效率。
如果存在跳转指令,那么预先取出的指令就无用了。cpu在执行当前指令时,从内存中取出了当前指令的下一条指令。执行完当前指令后,cpu发现不是要执行下一条指令,而是执行offset偏移处的指令。cpu只能重新从内存中取出offset偏移处的指令。因此,跳转指令会降低流水线的效率,也就是降低cpu的效率。

综上,在写程序时应该尽量避免跳转语句。那么如何避免跳转语句呢?答案就是使用__builtin_expect。

这个指令是gcc引入的,作用是”允许程序员将最有可能执行的分支告诉编译器”。

这个指令的写法为:builtin_expect(EXP, N)。意思是:EXP==N的概率很大。一般的使用方法是将builtin_expect指令封装为LIKELY和UNLIKELY宏。这两个宏的写法如下。

#define LIKELY(x) __builtin_expect(!!(x), 1) //x很可能为真
#define UNLIKELY(x) __builtin_expect(!!(x), 0) //x很可能为假

在很多源码如Linux内核、Glib等,我们都能看到likely()和unlikely()这两个宏,通常这两个宏定义是下面这样的形式。
可以看出这2个宏都是使用函数 builtin_expect()实现的, builtin_expect()函数是GCC的一个内建函数(build-in function).

2. 函数声明

函数__builtin_expect()是GCC v2.96版本引入的, 其声明如下:

long __builtin_expect(long exp, long c);

2.1. 功能描述

由于大部分程序员在分支预测方面做得很糟糕,所以GCC 提供了这个内建函数来帮助程序员处理分支预测.

你期望 exp 表达式的值等于常量 c, 看 c 的值, 如果 c 的值为0(即期望的函数返回值), 那么 执行 if 分支的的可能性小, 否则执行 else 分支的可能性小(函数的返回值等于第一个参数 exp).

GCC在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降, 达到优化程序的目的.

通常,你也许会更喜欢使用 gcc 的一个参数 ‘-fprofile-arcs’ 来收集程序运行的关于执行流程和分支走向的实际反馈信息,但是对于很多程序来说,数据是很难收集的。

2.2. 参数详解

  • exp
    exp 为一个整型表达式, 例如: (ptr != NULL)

  • c
    c 必须是一个编译期常量, 不能使用变量

2.3. 返回值

  返回值等于 第一个参数 exp

2.4. 使用方法

与关键字if一起使用.首先要明确一点就是 if (value) 等价于 if (__builtin_expert(value, x)), 与x的值无关.

例子如下:

例子1 : 期望 x == 0, 所以执行func()的可能性小

if (__builtin_expect(x, 0))
{
func();
}
else
{
  //do someting
}

例子2 : 期望 ptr !=NULL这个条件成立(1), 所以执行func()的可能性小

if (__builtin_expect(ptr != NULL, 1))
{  
  //do something
}
else
{
  func();
}

例子3 : 引言中的likely()和unlikely()宏

首先,看第一个参数!!(x), 他的作用是把(x)转变成”布尔值”, 无论(x)的值是多少 !(x)得到的是true或false, !!(x)就得到了原值的”布尔值”

使用 likely() ,执行 if 后面的语句 的机会更大,使用 unlikely(),执行 else 后面的语句的机会更大。

#define likely(x)    __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

int main(char *argv[], int argc)
{
int a;

/* Get the value from somewhere GCC can't optimize */
a = atoi (argv[1]);

if (unlikely (a == 2))
  {
a++;
}
else
  {
   a--;
  }
printf ("%d\n", a);

return 0;
}

3. RATIONALE(原理)

if else 句型编译后, 一个分支的汇编代码紧随前面的代码,而另一个分支的汇编代码需要使用JMP指令才能访问到.

很明显通过JMP访问需要更多的时间, 在复杂的程序中,有很多的if else句型,又或者是一个有if else句型的库函数,每秒钟被调用几万次,

通常程序员在分支预测方面做得很糟糕, 编译器又不能精准的预测每一个分支,这时JMP产生的时间浪费就会很大,

函数 __builtin_expert() 就是用来解决这个问题的.

1…2122232425262728293031323334353637
Mike

Mike

🚙 🚗 💨 💨 If you want to create a blog like this, just follow my open-source project, "hexo-theme-neo", click the GitHub button below and check it out ^_^ . It is recommended to use Chrome, Safari, or Edge to read this blog since this blog was developed on Edge (Chromium kernel version) and tested on Safari.

11 categories
290 posts
111 tags
about
GitHub Spotify
© 2013 - 2025 Mike