目录

最近在工作中在北极星sdk中发现了一个经常用到的指令__builtin_expect,正好借着这个机会学习一下与之相关的一些指令。不过有些确实比较鸡肋。

__builtin_expect是GCC编译器提供的一个内置函数,用于告诉编译器一个分支的执行概率,以便编译器在生成机器码时进行优化。它的语法如下:

__builtin_expect (long exp, long c)

__builtin_expect的使用背景是,现代CPU的分支预测机制可以预测分支的执行概率,从而提高程序的执行效率。但是,如果分支的执行概率与预测不符,就会导致CPU的流水线被清空,从而降低程序的执行效率。因此,为了让CPU的分支预测机制更加准确,我们可以使用__builtin_expect来告诉编译器一个分支的执行概率,从而让编译器生成更加优化的机器码。

一般使用的话我们需要封装一下宏

// 引入__builtin_expect函数来增加条件分支预测的准确性
#ifdef __GNUC__
#  define SDK_LIKELY(x) __builtin_expect(!!(x), 1)          // x很可能为真  
#  define SDK_UNLIKELY(x) __builtin_expect(!!(x), 0)        // x很可能为假
#else /* __GNUC__ */
#  define SDK_LIKELY(x) (x)
#  define SDK_UNLIKELY(x) (x)
#endif /* __GNUC__ */

#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    int sum = 0;
    for (int i = 0; i < 100000000; ++i) {
        if (SDK_LIKELY(i % 2 == 0)) {
            sum += i;
        } else {
            sum -= i;
        }
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Sum: " << sum << "\n";
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";
    return 0;
}

该代码耗时215ms

不加SDK_LIKELY耗时219ms,提升效果不是很明显

需要注意的是:

__builtin_expect指令只有在分支的执行概率与预测不符的情况下才能提高程序的执行效率。如果分支的执行概率与预测相符,那么__builtin_expect指令就没有任何作用。因此,如果程序中的分支都已经被CPU的分支预测机制正确预测了,那么使用__builtin_expect指令也无法提高程序的执行效率。

2、__builtin_prefetch

__builtin_prefetch是GCC编译器提供的一个内置函数,用于预取数据到CPU的缓存中,以便提高程序的执行效率。它的语法如下:

其中,addr是一个指向要预取数据的地址的指针,rw是一个表示读写属性的整数,locality是一个表示预取数据的局部性的整数。__builtin_prefetch的返回值是void类型,它只是告诉CPU预取数据到缓存中,而不会等待数据被加载到缓存中。

__builtin_prefetch的内部原理是,它会向CPU发送一个预取数据的请求,然后CPU会将请求加入到预取队列中。当CPU空闲时,它会从预取队列中取出请求,并将请求的数据预取到缓存中。

下面是一个demo,在求和前,先预取下一部分数据。

#include <iostream>
#include <chrono>

int main() {
    int* p = new int[100000000];
    for (int i = 0; i < 100000000; ++i) {
        p[i] = i;
    }
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 100000000; ++i) {
        __builtin_prefetch(&p[i + 100]);
        p[i] += 1;
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";
    delete[] p;
    return 0;
}

如果不使用__builtin_prefetch,耗时如下,提升效果还是挺明显的

[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 269ms
[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 302ms
[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 309ms
[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 251ms
[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 244ms
[hanhandi@VM-33-162-centos ~/hanhan_CppScripts/test]$ ./test 
Time: 270ms

如果预取的数据与程序的执行流程不符,就会导致CPU的缓存被清空,从而降低程序的执行效率。

如果编译器已经对程序进行了优化,那么使用__builtin_prefetch指令可能会导致程序的执行效率降低。

3、__builtin_unreachable

__builtin_unreachable是GCC和Clang编译器提供的一个内置函数,用于标记程序中不可能到达的代码路径。当程序执行到__builtin_unreachable时,编译器会认为程序已经进入了一个不可能到达的状态,从而优化程序的执行效率。

__builtin_unreachable的使用背景通常是在一些特殊的情况下,例如在switch语句中使用了default分支,但是程序永远不会执行到default分支,或者在一个函数中使用了noreturn属性,表示函数永远不会返回。在这些情况下,使用__builtin_unreachable可以告诉编译器程序已经进入了一个不可能到达的状态,从而优化程序的执行效率。

__builtin_unreachable的内部原理是通过向编译器发出一个不可到达的信号,具体来说,编译器会在__builtin_unreachable的位置插入一个不可到达的标记。在优化程序时,编译器会根据这个标记来判断程序的执行路径,从而优化程序的执行效率。

__builtin_unreachable的使用可能会有一些弊端。首先,如果程序中使用了__builtin_unreachable,但是实际上程序可以到达这个位置,就会导致程序出现未定义的行为。其次,如果程序中使用了__builtin_unreachable,但是实际上程序永远不会到达这个位置,就会导致程序的可读性降低。因此,在使用__builtin_unreachable时,需要确保程序的正确性,并且需要在代码中添加注释来解释为什么这个位置是不可到达的。

除了在switch语句中使用default分支和在noreturn函数中使用外,__builtin_unreachable还有一些更实用的用处。

一种常见的用法是在assert宏中使用__builtin_unreachable,用于标记assert宏的条件不可能为假的情况。例如:

#include <cassert>

void test(int x) {
    assert(x > 0 && "x must be greater than 0");
    // ...
    __builtin_unreachable();
}

在这个示例代码中,我们使用assert宏来判断x的值是否大于0。如果x的值小于等于0,则assert宏会触发一个断言错误,并输出"x must be greater than 0"。由于在这种情况下程序已经进入了一个不可能到达的状态,因此我们使用__builtin_unreachable来标记这个位置,从而告诉编译器程序已经进入了一个不可能到达的状态,从而优化程序的执行效率。

#include <iostream>
#include <cstdlib>

[[noreturn]] void error(const char* msg) {
    std::cerr << "Error: " << msg << std::endl;
    std::exit(1);
    __builtin_unreachable();
}

int main() {
    error("Something went wrong");
    return 0;
}

在这个示例代码中,我们定义了一个函数error,该函数接受一个字符串参数msg,并使用std::cerr输出错误信息。然后,我们使用std::exit函数退出程序,并使用__builtin_unreachable来标记程序已经进入了一个不可能到达的状态。

另外,__builtin_unreachable还可以用于标记一些不可能到达的错误处理代码。例如,在处理内存分配失败的情况时,我们可以使用__builtin_unreachable来标记内存分配失败后的错误处理代码。

不过总体来说,这个功能还是比较鸡肋的。

__builtin_popcount是GCC和Clang编译器提供的一个内置函数,用于计算一个整数中二进制位为1的个数。该函数的使用背景是在一些位运算和计算机视觉等领域中,需要对二进制数据进行处理和分析,而二进制位为1的个数是一个常见的计算需求。

__builtin_popcount的弊端是可能会导致代码的可移植性问题。由于__builtin_popcount是GCC和Clang编译器提供的一个内置函数,因此在使用__builtin_popcount时,需要确保代码的可移植性,并且需要在代码中添加条件编译来处理不支持POPCNT指令的CPU。另外,由于__builtin_popcount的实现依赖于CPU架构和编译器选项,因此在不同的平台和编译器下,__builtin_popcount的性能可能会有所不同。在使用__builtin_popcount时,需要进行性能测试,并根据实际情况选择最优的实现方式。

下面是一个demo

#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    int count = 0;
    for (int i = 0; i < 100000000; ++i) {
        count += __builtin_popcount(i);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Count: " << count << "\n";
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";
    return 0;
}
#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    long long count = 0;
    for (long long i = 0; i < 100000000; ++i) {
        count += __builtin_popcountll(i);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Count: " << count << "\n";
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";
    return 0;
}

5、__builtin_clz&__builtin_ctz & __builtin_clzll & __builtin_ctzll

__builtin_clz的内部原理是使用CPU的指令集来实现计算。具体来说,当CPU支持CLZ指令时,__builtin_clz会使用CLZ指令来计算从最高位开始连续的0的个数;否则,__builtin_clz会使用一些位运算技巧来实现计算。在实现中,__builtin_clz会根据不同的CPU架构和编译器选项来选择最优的实现方式,从而提高计算效率。

与之相似的是__builtin_ctz用于计算一个整数的二进制表示中,从最低位开始连续的0的个数。

__builtin_clzll 与 __builtin_ctzll则是64位对应的实现版本。

#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    int count_clz = 0;
    int count_ctz = 0;
    long long count_clzll = 0;
    long long count_ctzll = 0;
    for (int i = 0; i < 100000000; ++i) {
        count_clz += __builtin_clz(i);
        count_ctz += __builtin_ctz(i);
        count_clzll += __builtin_clzll(i);
        count_ctzll += __builtin_ctzll(i);
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "count_clz: " << count_clz << "\n";
    std::cout << "count_ctz: " << count_ctz << "\n";
    std::cout << "count_clzll : " << count_clzll << "\n";
    std::cout << "count_ctzll : " << count_ctzll << "\n";
    std::cout << "Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms\n";
    return 0;
}

6、__builtin_bswap16/32/64

__builtin_bswap16/32/64是GCC和Clang编译器提供的内置函数,用于交换一个整数的字节顺序。其中,__builtin_bswap16用于交换一个16位整数的字节顺序,__builtin_bswap32用于交换一个32位整数的字节顺序,__builtin_bswap64用于交换一个64位整数的字节顺序。这些函数的使用背景是在一些网络编程和底层编程中,需要对字节序进行处理和转换,而交换字节顺序是一个常见的操作。

__builtin_bswap16/32/64的内部原理是使用CPU的指令集来实现交换。具体来说,当CPU支持BSWAP指令时,__builtin_bswap16/32/64会使用BSWAP指令来实现交换;否则,__builtin_bswap16/32/64会使用一些位运算技巧来实现交换。在实现中,__builtin_bswap16/32/64会根据不同的CPU架构和编译器选项来选择最优的实现方式,从而提高交换效率。

__builtin_bswap16/32/64的弊端是可能会导致代码的可移植性问题。由于__builtin_bswap16/32/64是GCC和Clang编译器提供的内置函数,因此在使用__builtin_bswap16/32/64时,需要确保代码的可移植性,并且需要在代码中添加条件编译来处理不支持BSWAP指令的CPU。另外,由于__builtin_bswap16/32/64的实现依赖于CPU架构和编译器选项,因此在不同的平台和编译器下,__builtin_bswap16/32/64的性能可能会有差异。

下面给出一个demo,这里为了平台兼容性我们也实现了不支持__builtin_bswap的对应的函数。

在这个代码中,我们首先使用defined(__GNUC__) || defined(__clang__)来判断当前编译器是否是GCC或Clang。如果是,我们再使用defined(__x86_64__) || defined(__i386__)来判断当前CPU是否是x86或x86-64架构。如果是,我们就定义BSWAP_SUPPORTED宏,表示当前CPU支持BSWAP指令。

接下来,我们使用#ifdef BSWAP_SUPPORTED来判断当前CPU是否支持BSWAP指令。

打印

3412
78563412
f0debc9a78563412