Inline
宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
内联函数从 源代码层看,有函数的结构,而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看 编译器对该函数定义的具体处理。
优点:inline函数是一个真正的函数,它可以进行参数检测,相比较于普通函数,它的执行效率上更加快速。
缺点:浪费内存。inline函数在函数调用的地方会在预编译的时候生成一份函数的拷贝,也就是说,只要有调用inline函数的地方,就会生成一处拷贝。而普通的函数在函数调用的地方只是存储了此函数的地址。
diff #define
内联函数直接嵌入到目标代码中,宏是简单的做文本替换.
在内联函数内不容许用循环语句和开关语句。如有则编译器将该函数视为普通函数那样产生函数调用代码。
递归函数(本身调用本身)是不能做为内联函数的。
内联函数只适用于1——5行的小函数。对于含有不少语句的大函数,函数调用和返回的开销相对于来讲微不足道,因此没不要用内联函数。
inline 与 #define的区别:
(1)内联函数在运行时可调试,而宏定义不能够;递归
(2)编译器会对内联函数的参数类型作安全检查或自动类型转换(同普通函数),而宏定义则不会; 编译器
(3)内联函数能够访问类的成员变量,宏定义则不能; 编译
(4)在类中声明同时定义的成员函数,自动转化为内联函数。
实例
定义在类中的成员函数默认都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的。
class A
{
public:void Foo(int x, int y) { } // 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y){}
inline 是一种"用于实现的关键字"
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
如下风格的函数 Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
而如下风格的函数 Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起
所以说,inline 是一种"用于实现的关键字",而不是一种"用于声明的关键字"。
慎用 inline
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着"内联"这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如"偷偷地"执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline 不应该出现在函数的声明中)。
memcpy 优化
void BitSet(uint8_t* buffer_address, int32_t index) {
int64_t mask = 1L << (index & 0x3f); // mod 64 and shift
int64_t wordOffset = (index >> 6) * 8;
int64_t word;
memcpy(&word, buffer_address + wordOffset, sizeof(int64_t));
int64_t value = word | mask;
memcpy(buffer_address + wordOffset, &value, sizeof(int64_t));
}
可以优化成
inline void BitSet(uint8_t* buffer_address, int32_t index) {
int64_t mask = 1L << (index & 0x3f); // mod 64 and shift
int64_t wordOffset = (index >> 6) * 8;
int64_t word;
word = *(int64_t*)(buffer_address + wordOffset);
int64_t value = word | mask;
*(int64_t*)(buffer_address + wordOffset) = value;
}
常用的寄存器
有16个常用寄存器
rax、rbx、rcx 、rdx、rsi、rdi、rbp、rsp
r8、r9、r10、r11、r12、r13、r14、r15
寄存器的具体用途
- rax、rdx常作为函数返回值使用
- rdi、rsi、rdx、rcx、r8、r9等寄存器常用于存放函数参数
- rsp、rbp用于栈操作
- rip作为指令指针
存储着CPU下一条要执行的指令的地址
一旦CPU读取一条指令,rip会自动指向下一条指令(存储下一条指令的地址)
内存寻址
小括号表示这个内存地址存储的数据。
AT&T 和 Intel 汇编语法的主要区别
AT&T 语法先写源操作数,再写目标操作数;Intel 语法先写目标操作数,再写源操作数:
AT&T
movl %esp, %ebp
Intel
MOV EBP, ESP
各种取址方式的表示。AT&T 语法总体上是offset(base, index, width)的格式;Intel 语法总体上是[INDEX * WIDTH + BASE + OFFSET]的格式:
AT&T
movl 0x0100, %eax
movl (%esi), %eax
movl -8(%ebp), %eax
movl 0x0100(,%ebx,4), %eax
movl 0x8(%edx,%ebx,4), %eax
Intel
MOV EAX, [0100]
MOV EAX, [ESI]
MOV EAX, [EBP-8]
MOV EAX, [EBX*4+0100]
MOV EAX, [EDX+EBX*4+8]
AT&T 语法要在常数前加 $、在寄存器名前加 % 符号;Intel 语法没有相应的东西要加:
AT&T
subl $0x30, %eax
Intel
SUB EAX, 30
变址寻址
是以一个寄存器里的数值加上另一个寄存器里的数字 乘以一个比例因子(1,2,4,8)再加上一个常数得到最终地址,把地址上的值放到寄存器中
movl $0x2000, %eax # 立即数寻址
movl $0x2, %ebx # 立即数寻址
movl (,%eax,4), %ecx #比例变址寻址, 把地址0x8000(0x2000 *4)上的值放到ecx中
movl 6(,%eax,4), %ecx #比例变址寻址, 把地址0x8006(0x2000 *4+6)上的值放到ecx中
movl (%ebx,%eax,4), %ecx #变址寻址, 把地址0x8002(0x2000*4+2)上的值放到ecx中
movl 6(%ebx,%eax,4), %ecx #变址寻址, 把地址0x8008(0x2000*4+2+6)上的值放到ecx中
R2C & C2R Performance Profiling and Optimization
Performance Optimization
- Avoid if-else branch
- Inline function
- Utilize CPU cache
- Use AVX-512
- First Row Second Column Pattern
Optimize String/Binary type
- Buffer instead Builder
- Initialize all true once validity_buffer allocated, to avoid too many AppendToBitmap