C进阶指南(3):显式内联、矢量扩展、C的逸闻轶事

五、显式内联

函数代码可被直接集成到调用函数中,而非产生独立的函数目标和单个调用。可显式地使用 inline 限定符来指示编译器这么做。根据 section
6.7.4 of C standard
 inline 限定符仅建议编译器使得”调用要尽可能快”,并且“此建议是否有效由具体实现定义”

要用内联函数优点,最简单的方法是把函数定义为 static ,然后将定义放入头文件。

/* middle.h */
static inline int middle(int a, int b){
    return (b-a)/2;
}

独立的函数对象仍然可能被导出,但在翻译单元的外部它是不可见的。这种头文件被包含在多个翻译单元中,编译器可能为每个单元发射函数的多份拷贝。因此,有可能两个变量指向相同的函数名,指针的值可能不相等。

另一种方法是,既提供外部可连接的版本,也提供内联版本,两个版本功能相同,让编译器决定使用哪个。这实际上是内嵌限定符的定义:

在一个翻译单元中,若某个函数所有的文件范围都包含不带extern内联函数限定符,则此翻译单元中的定义是内联定义。一个内联定义不为函数提供extern定义,也不禁止其他翻译单元的external定义。一个内联定义为external定义提供一个可选项,翻译器可用它实现统一翻译单元中对函数的任意调用。调用函数时,使用内联定义或外联定义是不确定的。

对于函数的两个版本,我们可以把下面的定义放在头文件中:

/* middle.h */
inline int middle(int a, int b){
    return (b-a)/2;
}

然后在具体的源文件中,用extern限定符发射翻译单元中外部可链接的版本:

#include "middle.h"
extern int middle(int a, int b);

GCC编译器实现不同于上述译码方式。若函数由 inline 声明,GCC总是发射外部可链接的目标代码,并且程序中只存在一个这样的定义。若函数被声明为export inline的,GCC将永不为此函数发射外部可链接的目标代码。自GCC 4.3版本起,可使用-STD= c99的选项使能为内联定义使能C99规则。若C99的规则被启用,则定义GNUC_STDC_INLINE。之前描述的 static 使用方法不受GCC对内联函数解释的影响。如果你需要同时使用内联和外部可链接功能的函数,可考虑以下解决方案:

/* global.h */
#ifndef INLINE
# if __GNUC__ && !__GNUC_STDC_INLINE__
#  define INLINE extern inline
# else
#  define INLINE inline
# endif
#endif

头文件中有函数定义:

/* middle.h  */
#include "global.h"
INLINE int middle(int a, int b) {
  return (b-a)/2;
}

在某个具体实现的源文件中:

#define INLINE
#include "middle.h

若要对函数强制执行内联,GCC和Clang编译器都可用 always_inline 属性达成此目的。下面的例子中,独立的函数对象从未被发射。

/* cdefs.h */
# define __always_inline   inline __attribute__((always_inline))

/* middle.h */
#include <cdefs.h>
static __always_inline int middle(int a, int b) {
  return (b-a)/2;
}

一旦编译器内联失败,编译将因错误而终止。例如  Linux kernel 就使用这种方法。可在 cdefs.h 中上述代码中使用的 __always_inline 。

六、矢量扩展

许多微处理器(特别是x86架构的)提供单指令多数据(SIMD)指令集来使能矢量操作。例如下面的代码:

#include <stdint.h>
#include <string.h>
#define SIZE 8
int16_t a[SIZE], b[SIZE];

void addtwo(){
    int16_t i = 0;

    while (i < SIZE) {
        a[i] = b[i] + 2;
        i++;
    }
}

int main(){
    addtwo();
    return a[0];
}

addtwo 中的循环迭代 8 次,每次往数组 b 上加 2,数组 b 每个元素是 16 位的有符号整型。函数addtwo 将被编译成下面的汇编代码:

$ gcc -O2 auto.c -S -o auto_no.asm

addtwo:
.LFB22:
        .cfi_startproc
        movl    $0, %eax
.L2:
        movzwl  b(%rax), %edx
        addl    $2, %edx
        movw    %dx, a(%rax)
        addq    $2, %rax
        cmpq    $16, %rax
        jne     .L2
        rep
        ret
        .cfi_endproc

起初,0 写入到 eax 寄存器。标签 L2 标着循环的开始。b 的首个元素由 movzwl 指令被装入的32位寄存器 edx 前16位。
edx寄存器的其余部分填 0。然后 addl 指令往 edx 寄存器中 a 的第一个元素的值加 2 并将结果存在 dx 寄存器中。累加结果从
dx(edx 寄存器的低16位)复制到 a 的第一个元素。最后,显然存放了步长为 2 (占2个字节 – 16位)的数组的 rax 寄存器与数组的总大小(以字节为单位)进行比较。如果 rax 不等于16,执行跳到 L2 ,否则会继续执行,函数返回。

SSE2 指令集提供了能够一次性给 8 个 16 位整型做加法的指令 paddw。实际上,最现代化的编译器都能够自动使用如 paddw 之类的矢量指令优化代码。Clang 默认启用自动向量化。 GCC的编译器中可用 -ftree-vectorize 或
-O3 开关启用它。这样一来,向量指令优化后的 addtwo 函数汇编代码将会大有不同:

$ gcc -O2 -msse -msse2 -ftree-vectorize -ftree-vectorizer-verbose=5 auto.c -S -o auto.asm
addtwo:
.LFB22:
        .cfi_startproc
        movdqa  .LC0(%rip), %xmm0
        paddw   b(%rip), %xmm0
        movdqa  %xmm0, a(%rip)
        ret
        .cfi_endproc

;...

.LC0:
        .value  2
        .value  2
        .value  2
        .value  2
        .value  2
        .value  2
        .value  2
        .value  2

最显着的区别在于循环处理消失了。首先,8 个 16 位值为 2 整数被标记为 LC0,由 movdqa 加载到 xmm0 寄存器。然后paddw 把 b 的每个 16 位的元素分别加到 xmm0 中的多个数值 2上。结果写回到 a,函数可以返回。指令 movqda 只能用在由16个字节对齐的内存对象上。这表明编译器能够对齐两个数组的内存地址以提高效率。

数组的大小不必一定只是 8 个元素,但它必须以 16 字节对齐(需要的话,填充),因此也可以用 128 位向量。用内联函数也可能是一个好主意,特别是当数组作为参数传递的时候。因为数组被转换为指针,指针地址需要16字节对齐。如果函数是内联的,编译器也许能减少额外的对齐开销。

#include <stdint.h>

void __always_inline addtwo(int16_t* a, int16_t *b, int16_t size){
    int16_t i;

    for (i = 0; i < size; i++) {
        a[i] = b[i] + 2;
    }
}

int main(){
    const int16_t size = 1024;
    int16_t a[size], b[size];

    addtwo(a, b, size);
    return a[0];
}

循环迭代 1024 次,每次把两个长度为 16 比特的有符号整型相加。使用矢量操作的话,上例中的循环总数可减少到 128。但这也可能自动完成,在GCC环境中,可用 vector_size 定义矢量数据类型,用这些数据和属性显式指导编译器使用矢量扩展操作。此处列举出 emmintrin.h 定义的采用 SSE 指令集的多种矢量数据类型。

/* SSE2 */
typedef double __v2df __attribute__ ((__vector_size__ (16)));
typedef long long __v2di __attribute__ ((__vector_size__ (16)));
typedef int __v4si __attribute__ ((__vector_size__ (16)));
typedef short __v8hi __attribute__ ((__vector_size__ (16)));
typedef char __v16qi __attribute__ ((__vector_size__ (16)));

这是用 __v8hi 类型优化之前的示例代码后的样子:

#include <stdint.h>
#include <string.h>
#include <emmintrin.h>

static void __always_inline _addtwo(__v8hi *a, __v8hi *b, const int16_t sz){
    __v8hi c = {2,2,2,2,2,2,2,2};

    int16_t i;
    for (i = 0; i < sz; i++) {
        a[i] = b[i] + c;
    }
}

static void __always_inline addtwo(int16_t *a, int16_t *b, const int16_t sz){
    _addtwo((__v8hi *) a, (__v8hi *) b, sz/8);
}

int main(){
    const int16_t size = 1024;
    int16_t a[size], b[size];
    /* ... */

    addtwo(a, b, size);
    return a[0];
}

关键是把数据转到合适的类型(此例中为 __v8hi),然后由此调整其他的代码。优化的效果主要看操作类型和处理数据量的大小,可能不同情况的结果差异很大。下表是上例中 addtwo 函数被循环调用 1 亿次的执行时间:

Compiler Time
gcc 4.5.4 O2 1m 5.3s
gcc 4.5.4 O2 auto vectorized 12.7s
gcc 4.5.4 O2 manual 8.9s
gcc 4.7.3 O2 auto vectorized 25.s
gcc 4.7.3 O2 manual 8.9s
clang 3.3 O3 auto vectorized 8.1s
clang 3.3 O3 manual 9.5s

Clang 编译器自动矢量化得更快,可能是因为用以测试的外部循环被优化的更好。慢一点的 GCC 4.7.3在内存对齐(见下文)方面效率稍低。

int32_t i;
for(i=0; i < 100000000; i++){
    addtwo(a, b, size);
}

6.1 使用内建函数( Intrinsic Function)

GCC 和 Clang 编译器也提供了内建函数,用来显式地调用汇编指令。

确切的内建函数跟编译器联系很大。x86 平台下,GCC 和 Clang 编译器都提供了带有定义的头文件,通过 x86intrin.h 匹配 Intel 编译器的内建函数(即 GCC 和 Clang 用 Intel 提供的头文件,调用 Intel 的内建函数。译者注)。下表是含特殊指令集的头文件:

  • MMX: mmintrin.h
  • SSE: xmmintrin.h
  • SSE2: emmintrin.h
  • SSE3: mm3dnow.h
  • 3dnow: tmmintrin.h
  • AVX: immintrin.h

使用内建函数后,前面的例子可以改为:

#include <stdint.h>
#include <string.h>
#include <emmintrin.h>

static void __always_inline addtwo(int16_t *a, int16_t *b, int16_t size){

    int16_t i;
    __m128i c = _mm_set1_epi16(2);

    for (i = 0; i < size; i+=8) {
        __m128i bb = _mm_loadu_si128(b+i);  // movqdu b+i -> xmm0
        __m128i r = _mm_add_epi16(bb, c);   // paddw c + xmm0 -> xmm0
        _mm_storeu_si128(a+i, r);           // movqdu xmm0 -> a+i
    }
}

int main(){
    const int16_t size = 1024;
    int16_t a[size], b[size];
    /* ... */

    addtwo(a, b, size);
    return a[0];
}

当编译器产生次优的代码,或因代码中的 if 条件矢量类型不可能表达需要的操作时时,可能需要这种编写代码的方法。

6.2 内存对齐

注意到上个例子用了与 movqdu 而非 movqda (上面的例子里仅用 SIMD 产生的汇编指令使用的是 movqda。译者注)同义的 _mm_loadu_si128。这因为不确定 a 或 b 是否已按
16 字节对齐。使用的指令是期望内存对象对齐的,但使用的内存对象是未对齐的,这样肯定会导致运行错误或数据毁坏。为了让内存对象对齐,可在定义时用 aligned 属性指导编译器对齐内存对象。某些情况下,可考虑把关键数据按 64 字节对齐,因为 x86 L1 缓存也是这个大小,这样能提高缓存使用率。

#include <stdint.h>
#include <string.h>
#include <emmintrin.h>

static void __always_inline addtwo(int16_t *a, int16_t *b, int16_t size){

    int16_t i;
    __m128i c = _mm_set1_epi16(2) __attribute__((aligned(16)));

    for (i = 0; i < size; i+=8) {
        __m128i bb = _mm_load_si128(b+i);  // movqda b+i -> xmm0
        __m128i r = _mm_add_epi16(bb, c);   // paddw c + xmm0 -> xmm0
        _mm_store_si128(a+i, r);           // movqda xmm0 -> a+i
    }
}

int main(){
    const int16_t size = 1024;
    int16_t a[size], b[size] __attribute__((aligned(16)));
    /* ... */

    addtwo(a, b, size);
    return a[0];
}

考虑到程序运行速度,使用自动变量好过静态或全局变量,情况允许的话还应避免动态内存分配。当动态内存分配无法避免时,Posix 标准 和 Windows 分别提供了 posix_memalign 和_aligned_malloc 函数返回对齐的内存。

高效使用矢量扩展喊代码优化需要深入理解目标架构工作原理和能加速代码运行的汇编指令。这两个主题相关的信息源有  Agner`s CPU blog 和它的装订版 Optimization
manuals

七、逸闻轶事

本文最后一节讨论 C 编程语言里一些有趣的地方:

array[i] == i[array];

因为下标操作符等价于*(array + i),因此 array 和 i 是可交换的,二者等价。

$ gcc -dM -E - < /dev/null | grep -e linux -e unix
#define unix 1
#define linux 1

默认情况下,GCC 把 linux 和 unix 都定义为 1,所以一旦把其中一个用作函数名,代码就会编不过。

int x = 'FOO!';
short y = 'BO';

没错,字符表达式可扩展到任意整型大小。

x = i+++k;
x = i++ +k;

后缀自增符在加号之前被词法分析扫描到。

(即示例中两句等价,不同于 x = i +  (++k) 。译者注)

x = i+++++k; //error
x = i++ ++ +k; //error

y = i++ + ++k; //ok

词法分析查找可被处理的最长的非空格字符序列(C标准6.4节)。第一行将被解析成第二行的样子,它们俩都会产生关于缺少左值的错误,缺失的左值本应该被第二个自增符处理。

致谢

C进阶指南(3):显式内联、矢量扩展、C的逸闻轶事

时间: 2024-08-28 00:35:29

C进阶指南(3):显式内联、矢量扩展、C的逸闻轶事的相关文章

C语言进阶指南(3)丨显式内联、矢量扩展、C的逸闻轶事

五.显式内联 (想让)函数代码被直接集成到调用函数中,而非产生独立的函数目标和单个调用,可显式地使用 inline 限定符来指示编译器这么做.根据section 6.7.4 of C standardinline 限定符仅建议编译器使得”调用要尽可能快”,并且“此建议是否有效由具体实现定义” 要用内联函数优点的最简单方法是把函数定义为 static ,然后将定义放入头文件. /* middle.h */ static inline int middle(int a, int b){ return

内联函数详解

什么是内联性和外联函数 类的成员函数可以分为内联函数和外联函数.内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内.而说明在类体内,定义在类体外的成员函数叫外联函数.外联函数的函数体在类的实现部分. 内联函数在调用时不是像一般的函数那样要转去执行被调用函数的函数体,执行完成后再转回调用函数中,执行其后语句,而是在调用函数处用内联函数体的代码来替换,这样将会节省调用开销,提高运行速度. 内联函数与前面讲过的带参数的宏定义进行一下比较,它们的代码效率是一样的,但是内联函数要优于宏定义

C++如何处理内联虚函数

http://blog.csdn.net/hedylin/article/details/1775556 当一个函数是内联和虚函数时,会发生代码替换或使用虚表调用吗? 为了弄清楚内联和虚函数,让我们将它们分开来考虑.通常,一个内联函数是被展开的. class CFoo { private: int val; public: int GetVal() { return val; } int SetVal(int v) { return val=v; } }; 这里,如果使用下列代码: CFoo x

拷贝构造,深度拷贝,关于delete和default相关的操作,explicit,类赋初值,构造函数和析构函数,成员函数和内联函数,关于内存存储,默认参数,静态函数和普通函数,const函数,友元

 1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.第二种初始化的方式是直接在构造方法里面实现初始化. 案例如下: #include<iostream> //如果声明已经定义,边不会生成 class classA { private: int a; int b; public: //拷贝构造的规则,有两种方式实现初始化 //1.一个是通过在后面:a(x),b(y)的方式实现初始化 //2.第二种初始化的方式是直

(转)内联(inline)函数与虚函数(virtual)的讨论

本文转自: http://topic.csdn.net/t/20051220/09/4469273.html 函数的inline属性是在编译时确定的, 然而,virtual的性质是在运行时确定的,这两个不能同时存在,只能有一个选择,文件中的inline关键字只是对编译器的建议,编译器是否采纳是编译器的事情. 1.内联函数是个静态行为,而虚函数是个动态行为,他们之间是有矛盾的. 2.我们之所以能看到一些象内联函数的虚函数,是因为某个函数是否是内联函数不是由我们说的算,而是由编译器决定的.我们只能向

第6课 内联函数分析

1.  常量与宏回顾 (1)C++中的const常量可以替代宏常数定义,如: const int A = 3; ←→ #define A  3 (2)C++中是否有解决方案,可以用来替代宏代码片段呢? 2. 内联函数 2.1 内联函数的定义 (1)C++编译器可以将一个函数进行内联编译,被C++编译器内联编译的函数叫内联函数. (2)C++中使用inline关键字声明内联函数.如 inline int func(int a, int b) { return a < b ? a : b; } (3

内联函数(五)

或许我们在 C 语言中听说过内联函数,但是内联函数是首先在 C++ 中提出来的,可能现代的 C 编译器支持内联函数. C++ 中的 const 常量可以代替宏常数定义,如:const int A = 3: <==> #define A 3:那么在 C++ 中是否有解决方案代替宏代码片段呢?在 C++ 中推荐使用内联函数替代宏代码片段,使用 inline 关键字声明内联函数.内联函数声明时 inline 关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求. C++ 编译器可以将一个函数

转载:CSS的组成,三种样式(内联式,嵌入式,外部式),优先级

(仅供自己备份) 原文地址:http://blog.csdn.net/chq11106004389/article/details/50515717 CSS的组成 选择符/选择器+声明(属性+值) 选择符/选择器,指明网页中要应用样式规则的元素,如本例中是p是选择器,它的文字将变成蓝色,而其他的元素(如ol)不会受到影响 声明:当有多条声明时,中间可以英文分号";"分隔 --------------------------------------------------内联式CSS -

Squid显式代理实现内网上网

一.Squid显式代理上网 Squid显式代理,监听某地址的3128活着8080端口,需要内网用户在浏览器上手动设置代理配置.内网pc只需要保证能路由可达Squid监听的地址即可,无需配置dns,内网pc的所有访问流量都通过浏览器出去(包括DNS解析). 二.配置 拓扑: (该模式下squid单臂部署,路由模式也是没问题的,但需要保证squid能正常上网) ①安装squid  [[email protected] ~]#   yum  install  -y  squid ②配置squid的配置