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

五、显式内联

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

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

/* middle.h */

static inline int middle(int a, int b){

    return (b-a)/2;

}

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

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

If all of the file scope declarations for a function in a translation unit include the inline function specifier without extern, then the definition in that translation unit is an inline definition. An inline definition does not provide an external definition for the function, and does not forbid an external definition in another translation unit. An inline definition provides an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit. It is unspecified whether a call to the function uses the inline definition or the external definition.

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

(译者注:即gcc中的 extern inline,优先使用内联版本,允许外部版本的存在)

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

/* 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 亿次的执行时间:

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节)。第一行将被解析成第二行的样子,它们俩都会产生关于缺少左值的错误,缺失的左值本应该被第二个自增符处理。

博主是一个有着7年工作经验的架构师,对于c++,自己有做资料的整合,一个完整学习C语言c++的路线,学习资料和工具。可以进我的Q群7418,18652领取,免费送给大家。希望你也能凭自己的努力,成为下一个优秀的程序员!另外博主的微信公众号是:C语言编程学习基地,欢迎关注!

原文地址:https://www.cnblogs.com/L928/p/10665178.html

时间: 2024-08-30 02:47:26

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

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

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

go语音之进阶篇显式调用panic函数

1.显式调用panic函数 示例: package main import "fmt" func testa() { fmt.Println("aaaaaaaaaaaaaaaaa") } func testb() { //fmt.Println("bbbbbbbbbbbbbbbbbbbb") //显式调用panic函数,导致程序中断 panic("this is a panic test") } func testc() {

数据类型回顾——数据类型转换(显式和隐式)—JS学习笔记2015-6-3(第47天)

对于JS这种语言来说,因为它是一种动态类型语言,变量是没有类型的,可以随时赋予任意值. 但是,数据本身和各种运算是有类型的,因此运算时变量需要转换类型. 大多数情况下,这种数据类型转换是自动的,但是有时也需要手动强制转换. 首先看下强制类型转换(显式) 之前提到的Namber.parseInt.parseFloat 都是强制类型转换: 这里在看阮一峰博客(http://javascript.ruanyifeng.com/grammar/conversion.html#toc1) Number方法

C++中的显式类型转化

类型转化也许大家并不陌生,int i; float j; j = (float)i; i = (int)j; 像这样的显式转化其实很常见,强制类型转换可能会丢失部分数据,所以如果不加(int)做强制转换,严检查的编译会报错,宽检查的编译会报warning.在C语言中,指针是4字节或者8字节的,所以指针之间的强制转换在转换的时候就如同不同的整数类型之间的赋值,问题在于对该指针的使用上,必须确保该指针确实可以做出这样的强制转换.常见的情况是void*到不同的指针类型(比如内存分配,参数传递),cha

selenium 找不到元素 (显式等待 和隐式等待的区别)

selenium自动化页面元素不存在异常发生的原因有一下几点: (1)页面加载时间过慢,需要查找的元素程序已经完成但是页面还未加载成功.此时可以加载页面等待时间. (2)查到的元素没有在当前的iframe或者frame中.此时需要切换至对应的iframe或者frame中才行. (3)元素错误. 解决页面加载时间所引起的元素找不到,我们可以为页面设置加载时间.时间的设置分为以下三种: (1)显式等待 显示等待是针对于某个特定的元素设置的等待时间,如果在规定的时间范围内,没有找到元素,则会抛出异常,

C++关键字之explicit(显式)

C++ Code 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566   /* KeyWord_explicit.cpp C++关键字:explicit(显示) Author: Michael Joessy Date: 2017-06-07 Marks: 在C++程序中很少有人去使用explicit关键

android中的显式与隐式Intent

Intent是Android初学者比较难理解的一个东西.我这里以通俗易懂的语言和通俗易懂的代码,让初学者简单感受一下Intent. intent就是意图的意思.Intent分两种:显式(Explicit intent)和隐式(Implicit intent). 一.显式(设置Component) 显式,即直接指定需要打开的activity对应的类.以下多种方式都是一样的,实际上都是设置Component直接指定Activity类的显式Intent,由MainActivity跳转到SecondAc

编写高质量代码改善C#程序的157个建议——建议46:显式释放资源需继承接口IDisposable

建议46:显式释放资源需继承接口IDisposable C#中的每一个类型都代表一种资源,资源分为两类: 托管资源:由CLR管理分配和释放的资源,即从CLR里new出来的对象. 非托管资源:不受CLR管理的对象,如Windows内核对象,或者文件.数据库连接.套接字.COOM对象等. 如果我们的类型使用了非托管资源,或者需要显示地释放托管资源,那么就需要让类型继承接口IDisposable,这毫无例外.这相当于告诉调用者,类型资源是需要显示释放资源的,你需要调用类型的Dispose方法. 一个标

【转】Android理解:显式和隐式Intent---- try catch

原文网址:http://blog.csdn.net/xiao__gui/article/details/11392987 Intent是Android初学者比较难理解的一个东西.我这里以通俗易懂的语言和通俗易懂的代码,让初学者简单感受一下Intent. intent就是意图的意思.Intent分两种:显式(Explicit intent)和隐式(Implicit intent). 一.显式(设置Component) 显式,即直接指定需要打开的activity对应的类.以下多种方式都是一样的,实际