问题

C 语言有什么奇技淫巧?

回答
嘿,哥们,聊到 C 语言的“奇技淫巧”,这可就有意思了。这东西,说白了就是利用 C 语言一些不太直观,但又特别巧妙的特性,来达成一些别人想不到或者达不到的效果。很多时候,这些技巧能让你写出更精炼、更高效的代码,当然了,用不好也容易把自己绕进去。

我这里给你掰扯几个比较典型的,保证不像是那种写流水账的AI文章,咱们就聊点实在的,带点江湖气儿。

1. 位运算的乾坤大挪移

说到 C 语言的“奇技淫巧”,位运算绝对是绕不开的头牌。这玩意儿,就是直接跟数字的二进制位打交道,玩得溜了,能把事儿办得又快又省。

检查一个数是不是偶数/奇数:
这玩意儿,估计你都听过,但想想看,为啥这么干?
```c
int num = 10;
if (num & 1) {
printf("%d 是奇数 ", num);
} else {
printf("%d 是偶数 ", num);
}
```
解释一下:任何一个数的二进制表示,最低位(最右边那一位)是 0 就代表偶数,是 1 就代表奇数。 `& 1` 这个操作,就是把除了最低位之外的其他位都变成 0,只留下最低位。如果最低位是 1,结果就是 1(真),否则就是 0(假)。这比 `num % 2 == 0` 或者 `num % 2 != 0` 都要快一点,因为位运算是 CPU 最底层的操作,指令周期短。

交换两个变量的值,不要临时变量:
这估计是很多教程里都会提到的一个“骚操作”。
```c
int a = 5, b = 10;
printf("交换前: a = %d, b = %d ", a, b);

a = a ^ b; // a 现在是 a 和 b 的异或结果
b = a ^ b; // b 现在是 (a ^ b) ^ b,因为 x ^ x = 0,所以 b 变回了原来的 a
a = a ^ b; // a 现在是 (a ^ b) ^ a (原来的 a),结果就是原来的 b

printf("交换后: a = %d, b = %d ", a, b);
```
这个技巧是利用了异或(`^`)运算的几个特性:
`x ^ x = 0` (任何数和它自己异或等于 0)
`x ^ 0 = x` (任何数和 0 异或等于它本身)
`x ^ y = y ^ x` (异或运算可交换)
`x ^ y ^ x = y` (符合结合律,同时可以抵消)
所以,`a = a ^ b` 之后,`a` 存储了 `a` 和 `b` 的“混合信息”。第二次 `b = a ^ b`,实际上就是 `b = (原来的 a ^ 原来的 b) ^ 原来的 b`,由于 `b ^ b = 0`,所以 `b` 就变成了 `原来的 a`。最后一次 `a = a ^ b`,就是 `a = (原来的 a ^ 原来的 b) ^ 新的 b (即原来的 a)`,结果就是 `原来的 b`。

重点: 这个技巧虽然炫酷,但不推荐在实际项目中大量使用,除非你的编译器对 `x ^ y ^ x` 这种写法有特殊的优化。现代编译器通常能很好地优化带有临时变量的交换操作,而且可读性更强。但作为理解位运算的绝佳例子,绝对够格。

设置、清除、翻转某个位:
这在操作硬件寄存器或者实现一些数据结构时非常有用。
假设我们要操作一个 `flag` 变量的第 `n` 位:
设置第 `n` 位为 1: `flag |= (1 << n);`
`1 << n` 会生成一个只有第 `n` 位是 1 的数。`|=` (按位或赋值) 操作,就是把 `flag` 的第 `n` 位强制设为 1,其他位不变。
清除第 `n` 位为 0: `flag &= ~(1 << n);`
`~(1 << n)` 会生成一个除了第 `n` 位是 0,其他位都是 1 的数。`&=` (按位与赋值) 操作,就是把 `flag` 的第 `n` 位强制设为 0,其他位不变。
翻转第 `n` 位: `flag ^= (1 << n);`
`^=` (按位异或赋值) 操作,如果第 `n` 位是 0,`0 ^ 1 = 1` 就变成 1;如果第 `n` 位是 1,`1 ^ 1 = 0` 就变成 0。完美实现翻转。

2. 指针的玄妙:内存的灵魂舞者

C 语言的指针,是它的强大之处,也是它的噩梦来源。玩得溜了,能直接操纵内存,效率炸裂;玩不好,分分钟让你怀疑人生。

函数指针:让函数成为数据
这玩意儿能让你的函数像变量一样被传递和调用。
```c
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a b; }

int main() {
int (operation)(int, int); // 声明一个函数指针

operation = add; // 指向 add 函数
printf("Add: %d ", operation(5, 3)); // 调用 add

operation = subtract; // 指向 subtract 函数
printf("Subtract: %d ", operation(5, 3)); // 调用 subtract

return 0;
}
```
这里 `int (operation)(int, int);` 声明了一个指针 `operation`,它指向一个函数,这个函数接收两个 `int` 类型参数,并返回一个 `int` 类型值。通过改变 `operation` 指向的函数,就能动态地改变程序行为。

应用场景:回调函数(callback functions)、策略模式(strategy pattern)、实现通用的排序算法(如 `qsort`)。

void 指针:万能钥匙?
`void ` 可以指向任何类型的数据,但是不能直接进行解引用和算术运算。必须先强制类型转换为具体类型。
```c
int i = 10;
char c = 'A';
void ptr;

ptr = &i
printf("Value from void pointer (as int): %d ", (int )ptr); // 强制转为 int

ptr = &c
printf("Value from void pointer (as char): %c ", (char )ptr); // 强制转为 char
```
它的奇妙之处在于通用性:比如实现一个可以处理任意数据类型的链表或栈,就可以用 `void ` 来存储数据。但切记,一旦类型搞错,就是严重的运行时错误。

指针的算术运算:不是简单的加减法
当你对指针进行加减运算时,它不是按照字节数来加减,而是按照它指向的数据类型的大小来加减。
```c
int arr[5] = {1, 2, 3, 4, 5};
int p = arr; // p 指向 arr[0]

printf("%p ", p); // 输出 arr[0] 的地址
printf("%p ", p + 1); // 输出 arr[1] 的地址,地址值会增加 sizeof(int) 个字节
printf("%p ", (char )p + 1); // 输出 arr[0] 地址加 1 个字节的地址
```
这个特性是 C 语言能如此高效操作数组和内存的关键。

3. 结构体和联合体的巧妙运用

结构体内存对齐的“陷阱”与“技巧”
编译器为了提高访问效率,会对结构体成员进行内存对齐。这会导致结构体的大小可能比所有成员加起来还要大。
```c
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
printf("sizeof(S1) = %zu ", sizeof(struct S1)); // 可能输出 12
printf("sizeof(S2) = %zu ", sizeof(struct S2)); // 可能输出 8
```
S1 中,`c1` (1字节) + 假对齐字节 (3字节) + `i` (4字节) + `c2` (1字节) + 假对齐字节 (3字节) = 12字节。
S2 中,`c1` (1字节) + `c2` (1字节) + 假对齐字节 (2字节) + `i` (4字节) = 8字节。
奇妙之处在于:
1. 减小结构体体积: 我们可以通过调整成员的顺序来优化内存使用,比如把小的成员放在一起。
2. 利用对齐空隙: 在某些特殊情况下,我们可以利用对齐产生的空隙来存储其他数据,虽然这通常不推荐,但了解其原理可以避免一些低级错误。
3. 控制对齐(GCC/Clang): 使用 `__attribute__((packed))` 可以取消结构体的对齐,让其成员紧密排列,但这会牺牲访问速度。

联合体(Union):一处内存,多种解释
联合体允许你在同一块内存区域存储不同类型的数据,但同一时间只能存储其中一种类型。
```c
union Data {
int i;
float f;
char str[20];
};

union Data data;
data.i = 10;
printf("data.i : %d ", data.i);

data.f = 22.5;
printf("data.f : %f ", data.f);
// printf("data.i : %d ", data.i); // 这里再访问 data.i 是不确定的,因为刚才存的是 float
```
奇妙之处在于:
1. 节省内存: 当你只需要存储几种类型数据中的一种时,可以用联合体来节省内存。
2. 类型标记(Tagging): 结合一个枚举或整数来记录当前联合体中存储的是哪种类型的数据,这是一种常见的数据结构模式,比如在解析网络协议或文件格式时很有用。
3. 位域(Bitfields): 虽然不是直接的联合体技巧,但位域可以看作是结构体/联合体的一种特殊形式,允许你精确控制每个变量占用的位数,这在处理硬件寄存器时非常有用。

4. 宏定义的“魔法”

宏定义在 C 语言里,就像是个万能的“文本替换器”,但用好了,能写出非常灵活的代码。

条件编译:`ifdef` / `ifndef` / `if`
这是宏最基础也最重要的用法之一,让你可以在不同的编译环境下生成不同的代码。
```c
define DEBUG_MODE

int main() {
// ...

ifdef DEBUG_MODE
printf("Debug mode is enabled. ");
endif

// ...
}
```
这能帮你轻松切换调试代码和发布代码,或者根据操作系统、编译器等条件编译不同的模块。

“麻醉”操作符的副作用:`dowhile(0)` 宏
这是一个非常经典的宏技巧,用来创建一个可以安全使用的多语句宏。
```c
define SWAP(a, b) do { int temp = a; a = b; b = temp; } while (0)

int main() {
int x = 5, y = 10;
if (x > 0)
SWAP(x, y); // 如果没有 dowhile(0),这相当于 if (x > 0) { int temp = x; x = y; y = temp; }
// 而如果没有大括号,else 会和 SWAP 中的第一条语句绑定,导致错误
else
printf("y is greater than x. ");

printf("x = %d, y = %d ", x, y);
return 0;
}
```
如果没有 `dowhile(0)` 和大括号,当宏被用在 `if` 或 `else` 的单语句分支时,后面的 `else` 或者其他语句就会和宏的某个部分绑定在一起,造成语法错误或者逻辑错误。`dowhile(0)` 就把宏内的所有语句“打包”成一个整体,像一个独立的语句块一样,非常安全。

字符串化(Stringizing)和令牌粘贴(Token Pasting)
字符串化 (``): 将宏参数转换为字符串字面量。
```c
define STRINGIFY(x) x
printf("%s ", STRINGIFY(Hello World)); // 输出 "Hello World"
```
这在日志输出、错误信息生成时很有用。
令牌粘贴 (``): 将两个宏参数连接起来形成一个新的标识符。
```c
define CONCAT(a, b) a b
int my_var_123 = 100;
int result = CONCAT(my_var_, 123); // result 就变成了 my_var_123 的值
printf("%d ", result);
```
这可以用来动态生成变量名、函数名(虽然不常见),或者创建命名约定一致的标识符。

5. 强制类型转换:潜规则的执行者

C 语言的类型转换,尤其是那些看起来很“野蛮”的强制类型转换,也是“奇技淫巧”的重要组成部分。

指针的强制转换:
前面 `void ` 已经讲了,但更进一步的,比如将 `char ` 强制转换为 `int ` 来读取内存中的整数表示,或者反过来。
```c
int num = 0x12345678; // 假设是 32 位整数
unsigned char byte_ptr = (unsigned char )#

printf("Bytes of %x: ", num);
for (int i = 0; i < sizeof(int); ++i) {
printf("%02x ", byte_ptr[i]);
}
printf(" ");
```
这里我们打印出 `num` 在内存中存储的各个字节。这对于理解字节序(大端/小端)非常重要。

函数返回值和参数的转换:
虽然编译器会进行隐式转换,但显式转换能更清晰地表达意图,有时也能解决一些棘手的问题。例如,在进行浮点数和整数混合运算时,显式转换可以控制运算的精度和结果。

6. 了解底层,操控细节

内联汇编(Inline Assembly):
虽然不是纯 C 语言的技巧,但在 C 中嵌入汇编代码,能让你做到 C 语言做不到的极致优化或者直接访问硬件指令。
在 GCC/Clang 中:
```c
int x = 10, y = 20, result;
asm ("add %1, %2, %0" : "=r"(result) : "r"(x), "r"(y)); // result = x + y
printf("Result of inline assembly: %d ", result);
```
这里 `"=r"(result)` 表示 `result` 是输出,使用通用寄存器;`"r"(x)` 和 `"r"(y)` 表示 `x` 和 `y` 是输入,也使用通用寄存器。`asm` 语句后的冒号后面是输入输出的约束和变量。
这绝对是“奇技淫巧”的最高境界了,直接操作 CPU 指令。

总结一下

C 语言的这些“奇技淫巧”,其实就是利用了它的低级性、灵活性和直接内存访问能力。
位运算让你能精细控制数据。
指针让你能操纵内存地址,实现动态和高效的数据处理。
结构体/联合体让你能灵活管理数据结构和内存布局。
宏提供了强大的代码生成和文本替换能力。
强制类型转换能突破类型限制,直接处理底层数据。
内联汇编更是直接与硬件对话。

但最重要的一点是: 这些技巧虽然强大,可读性和可维护性往往会大幅下降。在实际工作中,除非有明确的性能瓶颈或者必须的底层操作需求,否则优先选择清晰、易懂、可维护的代码。这些“奇技淫巧”更多的是一种知识储备,一种解决问题的思路,让你在遇到某些极端情况时能有“杀手锏”。

玩 C 语言,就像是在武林中行走,知道这些“绝世武功”总归是好事,但记得别走火入魔,别为了炫技而牺牲代码的稳定性和可读性。希望这些分享能让你对 C 语言有更深的认识!

网友意见

user avatar

C有一个鲜为人知的运算符叫”趋向于”, 写作“-->”。比如说如果要实现一个倒数的程序,我们可以定义一个变量x,然后让它趋向与0:

       #include <stdio.h>  int main(int argc, char** argv) {   int x = 10;   while (x --> 0) {     printf("%d ", x);   }   return 0; }     

会打印出:

       9 8 7 6 5 4 3 2 1 0     

----------------

好吧我承认我是来恶搞的。。。不过程序真的能run。

评论里说我应该加上参考文献,所以去找了一下我最开始看到这个的stackoverflow - 这个链接里面还有更多脑洞大开的解释。。。

[1]

c++ - What is the name of the "-->" operator?
user avatar

补充一个,

XOR linked list

原理很简单,利用C的按位异或只用一个字节的指针信息就实现双向链表

除了开头和结尾,每个节点保存其相邻节点的地址的异或结果,正向遍历时用当前节点地址字段中保存的值异或后一个,反向遍历时异或前一个。

而开头节点存下一个节点地址,尾节点保存前一节点地址。

这样一来,用一个接口就能实现链表双向遍历,还比双链表节省空间

user avatar

自定义"控制流"

假设我们要打开一个文件读写, 读写完毕把文件关闭, 并且加一点错误处理, 那么代码会像这样:

       int f = open("foo.txt", O_RDWR); if (f >= 0) {   ...   close(f); // 忘记 close 怎么办? } else {   // 错误处理 }     

如果像 Ruby 一样, block 结束可以自动关闭就好啦! 利用 for 语句的执行顺序我们居然可以做到:

       #define OPEN(f, ...)   for (int f = open(__VA_ARGS__), _m = 1; _m; _m--, f >= 0 && close(f))     if (f >= 0)  OPEN(f, "foo.txt", O_RDWR) {   ... // 自动关闭, 再也不用手动调用 close 了 } else {   // 错误处理 }     

实现 foreach ... in

有了上面的自定义控制流,我们定义一个宏 in,让它展开后变成 ,,然后模拟 foreach 语法行不行?像这样:

       #define in , #define foreach(e, a) for(int i = 0, elem* e = a->elems; i != a->size; i++, e++)  ...  foreach(e in a) { // 编译不通过 >_< }     

但上面代码通不过编译,编译器在展开 foreach 时还没有展开 in,而 foreach 是 function-like macro,校验参数个数就当成一个参数计算,然后就败了。解决此的奇技淫巧就是在宏里包括号,这样就能跳过 function-like 的校验先把 in 给展开了:

       #define in , #define foreach(...) foreach_ex(foreach_in, (__VA_ARGS__)) #define foreach_ex(m, wrapped_args) m wrapped_args #define foreach_in(e, a) for(int i = 0, elem* e = a->elems; i != a->size; i++, e++)  foreach(e in a) { // 好使了 }     

单元测试 DSL

我们甚至可以让 C 的单元测试写起来像这样:

       void your_suite() {    ccut_test(foo1) {     assert_true(2 == 2, "wat?");   }    ccut_test(foo2) {     assert_false(1 == 2, "no way!");   }    ccut_test(bar) {     pending;   }    ccut_test(simple equal) {     assert_eq(expected, actual);   }  }     

这里除了使用“自定控制流”的技巧以外,还用到了 coroutine 的技术,详见 github.com/luikore/ccut

"类型推断"

我们知道 C++1x 可以用 auto 来省略类型声明, 而 C 的 auto 关键字意思完全不同但明显是个废话(auto 的意思是非 static, 和不写一样), 还好有 C 有 __typeof__

                #define var(left, right) __typeof__(right) left = (right)             


然后用起来就像这样:

       var(s, 1LL); // 相当于 long long s = 1LL;      


宏参数单次求值

我们知道 C 的宏会把给的参数原样拷贝到 macro 体内, 所以宏里经常要多加好多括号很麻烦, 而且如果参数在宏内出现多次的话就会被求值多次, 例如下面的宏 DOUBLE 就有这样的问题

       #define DOUBLE(a) ((a) + (a)) int foo() {   printf(__func__);   return 3; } int main () {   DOUBLE(foo()); // 调用了两次 foo() }     


在 GCC/Clang 中, 利用 __typeof__ 与局部变量, 就可以让宏参数只被求值一遍

                #define DOUBLE(a) ({            __typeof__(a) _x_in_DOUBLE = (a);            _x_in_DOUBLE + _x_in_DOUBLE;          })             


细心的你会发现为什么要用 _x_in_DOUBLE 这种蹊跷的名字呢... 因为如果表达式 a 带有一个变量恰好和宏里声明的局部变量同名, 你就挂了...

把 GCC 去掉, 只用 Clang/OpenCL 的话, 利用 lambda 表达式字面量, 这个问题终于能完美简单的解决了:

                #define DOUBLE(a) (^(__typeof__(a) x){ return x + x; }(a))            


所以用 Clang 写 C 真的很 high...

诱导常用选择支 LIKELY

很多编译器都会提供一些 builtin 函数来帮助帮助优化, 有些是直接调用能完成复杂操作的 CPU 指令例如 popcnt, aes256, crc32, 有些是指导生成代码.

比较常见的例如 __builtin_expect 就是帮助做编译期分支预测的, 它没有执行效果, 只会帮助编译器把不常用的分支代码移离正常执行路径, 来提高执行速度.

由于某些编译器没有这个内部函数, 或者直接用 PGO (profile guided optimization) 也能达到这个效果, 所以就定义为 LIKELY 和 UNLIKELY 宏可以随时关掉好了

                #ifndef __GNUC__          #define __builtin_expect(x, expected_value) (x)          #endif          #define LIKELY(x)    __builtin_expect(!!(x),1)          #define UNLIKELY(x)  __builtin_expect((x)!=0,0)             


使用例子: malloc 出 NULL 的情况真的很少, 那么我们可以

       ptr = malloc(size); if (UNLIKELY(ptr == NULL)) {   ... some dirty work ... }     


其他的, 相信看这个问答更有用:

What is your favorite C programming trick?

user avatar

有很多啊

快速范围判断

经常要批量判断某些值在不在范围内,如果 int 检测是 [0, N) 的话:

       if (x >= 0 && x < N) ...      

众所周知,现代 CPU 优化,减分支是重要手段,上述两次判断可以简写为:

       if (((unsigned int)x) < N) ...      

减少判断次数。如果 int 检测范围是 [minx, maxx] 这种更常见的形式的话,怎么办呢?

       if (x >= minx && x <= maxx) ...      

可以继续用比特或操作继续减少判断次数:

       if (( (x - minx) | (maxx - x) ) >= 0) ...      

如果语言警察们担心有符号整数回环是未定义行为的话,可以写成这样:

       if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...      

性能相同,但避开了有符号整数回环,改为无符号回环,合并后转为有符号判断最高位。

第一个 (x - minx) 如果 x < minx 的话,得到的结果 < 0 ,即高位为 1,第二个判断同理,如果超过范围,高位也为 1,两个条件进行比特或运算以后,只有两个高位都是 0 ,最终才为真,同理,多个变量范围判断整合:

       if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...      

这样本来需要对 [x, y] 进行四次判断的,可以完全归并为一次判断,减少分支。

补充:加了个性能评测:

性能提升 37%。快速范围判断还有第二个性能更均衡的版本:

       if ((unsigned)(x - minx) <= (unsigned)(maxx - minx)) ...      

快速范围判断的原理和评测详细见:《快速范围判断:再来一种新写法》。


更好的循环展开

很多人提了 duff's device ,按照 gcc 和标委会丧心病狂的程度,你们用这些 just works 的代码,不怕哪天变成未定义行为给一股脑优化掉了么?其实对于循环展开,可以有更优雅的写法:

                #define CPU_LOOP_UNROLL_4X(actionx1, actionx2, actionx4, width) do {               unsigned long __width = (unsigned long)(width);                  unsigned long __increment = __width >> 2;               for (; __increment > 0; __increment--) { actionx4; }                  if (__width & 2) { actionx2; }               if (__width & 1) { actionx1; }           }   while (0)             

送大家个代替品,CPU_LOOP_UNROLL_4X,用于四次循环展开,用法是:

       CPU_LOOP_UNROLL_4X(     {         *dst++ = (*src++) ^ 0x80;     },     {         *(uint16_t*)dst = (*(uint16_t*)src) ^ 0x8080;         dst += 2; src += 2;     },     {         *(uint32_t*)dst = (*(uint32_t*)src) ^ 0x80808080;         dst += 4; src += 4;     },     w);      

假设要对源内存地址内所有字节 xor 0x80 然后复制到目标地址的话,可以向上面那样进行循环展开,分别写入 actionx1, actionx2, actionx4 即:单倍工作,双倍工作,四倍工作。然后主体循环将用四倍工作的代码进行循环,剩余长度用两倍和单倍的工作拼凑出来。

现在的编译器虽然能够帮你展开一些循环,CPU 也能对短的紧凑循环有一定预测,但是做的都非常傻,大部分时候你用这样的宏明确指定循环展开循环效果更好,你还可以再优化一下,主循环里每回调用两次 actionx4,这样还能少一半循环次数,剩余的用其他拼凑。

这样比 duff's device 这种飞线的写法更规范,并且,duff's device 并不能允许你针对 “四倍工作”进行优化,比如上面 actionx4 部分直接试用 uint32_t 来进行一次性运算,在 duff's device 中并没有办法这么做。

补充:《循环展开性能评测》:

性能提升 12% 。

整数快速除以 255

整数快速除以 255 这个事情非常常见,例如图像绘制/合成,音频处理,混音计算等。网上很多比特技巧,却没有人总结过非 2^n 的快速除法方法,所以我自己研究了个版本:

                #define div_255_fast(x)    (((x) + (((x) + 257) >> 8)) >> 8)             

当 x 属于 [0, 65536] 范围内,该方法的误差为 0。过去不少人简略的直接用 >> 8 来代替,然而这样做会有误差,连续用 >>8 代替 / 255 十次,误差就累计到 10 了。

上面的宏可以方便的处理 8-16 位整数的 /255 计算,经过测试 65536000 次计算中,使用 /255的时间是 325ms,使用div_255_fast的时间是70ms,使用 >>8 的时间是 62ms,div_255_fast 的时间代价几乎可以忽略。

进一步可以用 SIMD 写成:

       // (x + ((x + 257) >> 8)) >> 8 static inline __m128i _mm_fast_div_255_epu16(__m128i x) {  return _mm_srli_epi16(_mm_adds_epu16(x,    _mm_srli_epi16(_mm_adds_epu16(x, _mm_set1_epi16(0x0101)), 8)), 8); }      

这样可以同时对 8 对 16 bit 的整数进行 / 255 运算,照葫芦画瓢,还可以改出一个 / 65535 ,或者 / 32767 的版本来。

对于任意大于零的整数,他人总结过定点数的方法,x86 跑着一般,x64 下还行:

       static inline uint32_t fast_div_255_any (uint32_t n) {     uint64_t M = (((uint64_t)1) << 32) / 255;   // 用 32.32 的定点数表示 1/255     return (M * n) >> 32;   // 定点数乘法:n * (1/255) }      

这个在所有整数范围内都有效,但是精度有些不够,所以要把 32.32 的精度换成 24.40 的精度,并做一些四舍五入和补位:

       static inline uint32_t fast_div_255_accurate (uint32_t n) {     uint64_t M = (((uint64_t)1) << 40) / 255 + 1;   // 用 24.40 的定点数表示 1/255     return (M * n) >> 40;   // 定点数乘法:n * (1/255) }      

该方法能够覆盖所有 32 位的整数且没有误差,有些编译器对于常数整除,已经可以生成类似 fast_div_255_accurate 的代码了,整数除法是现代计算机最慢的一项工作,动不动就要消耗 30 个周期,常数低的除法除了二次幂的底可以直接移位外,编译器一般会用定点数乘法模拟除法。

编译器生成的常数整除代码主要是使用了 64 位整数运算,以及乘法,略显复杂,对普通 32 位程序并不是十分友好。因此如果整数范围属于 [0, 65536] 第一个版本代价最低。

且 SIMD 没有除法,如果想用 SIMD 做除法的话,可用上面的两种方法翻译成 SIMD 指令。

255 快除法的《性能评测》:

提升一倍的性能。

--

PS:大部分时候当然选择相信编译器,提高可读性,如果你只写一些增删改查,那怎么漂亮怎么写就行;但如果你想写极致性能的代码,你需要知道编译器的优化是有限的穷举,没法应对无限的代码变化,上面三个就是例子,编译器优化可以帮你,但没法什么都靠编译器,归根结底还是要了解计算机体系,这样脱开编译器,不用 C 语言,你也能写出高性能代码。

PS:不要觉得丧心病狂,你们去看看 kernel 里各处性能相关的代码,看看 pypy 如何优化 python 的哈希表的,看看 jdk 的代码,这类优化比比皆是,其实写多了你也不会觉得难解。

--

常数范围裁剪

有时候你计算一个整数数值需要控制在 0 - 255 的范围,如果小于 0 那么等于零,如果大于 255,那么等于 255,做一个裁剪工作,可以用下面的位运算:

       static inline int32_t clamp_to_0(int32_t x) {   return ((-x) >> 31) & x;  } static inline int32_t clamp_to_255(int32_t x) {  return (((255 - x) >> 31) | x) & 255; }      

这个方法可以裁剪任何 2^n - 1 的常数,比如裁剪 65535:

       static inline int32_t clamp_to_65535(int32_t x) {  return (((65535 - x) >> 31) | x) & 65535; }      

略加改变即可实现,没有任何判断,没有任何分支。本技巧在不同架构下性能表现不一,具体看实测结果。

快速位扫描

假设你在设计一个容器,里面的容量需要按 2 次幂增加,这样对内存更友好些,即不管里面存了多少个东西,容量总是:2, 4, 8, 16, 32, 64 的刻度变化,假设容量是 x ,需要找到一个二次幂的新容量,刚好大于等于 x 怎么做呢?

       static inline int next_size(int x) {     int y = 1;     while (y < x) y *= 2;     return y; }      

一般会这样扫描一下,但是最坏情况上面循环需要迭代 31 次,如果是 64 位系统,类型是 size_t 的话,可能你需要迭代 63 次,假设你做个内存分配,分配器大小是二次幂增长的,那么每次分配都要一堆 for 循环来查找分配器大小的话,实在太坑爹了,于是继续位运算:

       static inline uint32_t next_power_of_2(uint32_t x) {     x--;     x |= x >> 1;      x |= x >> 2;      x |= x >> 4;      x |= x >> 8;      x |= x >> 16;      x++     return x; }      

以及:

       static inline uint32_t next_power_of_2(uint64_t x) {     x--;     x |= x >> 1;      x |= x >> 2;      x |= x >> 4;      x |= x >> 8;      x |= x >> 16;      x |= x >> 32;      x++     return x; }      

在不用 gcc 内置 __builtin_clz 函数或 bsr 指令的情况下,这是 C 语言最 portable 的方案。


。。。。

待续

类似的话题

  • 回答
    嘿,哥们,聊到 C 语言的“奇技淫巧”,这可就有意思了。这东西,说白了就是利用 C 语言一些不太直观,但又特别巧妙的特性,来达成一些别人想不到或者达不到的效果。很多时候,这些技巧能让你写出更精炼、更高效的代码,当然了,用不好也容易把自己绕进去。我这里给你掰扯几个比较典型的,保证不像是那种写流水账的A.............
  • 回答
    Python 和 C 语言,这两门语言可以说是编程界的两座高峰,它们各自拥有庞大的用户群体和广泛的应用领域,但它们在设计理念、语法特性、执行方式乃至学习曲线等方面,都存在着显著的差异。理解这些不同,对于选择合适的工具、深入学习编程至关重要。咱们先从它们的“出身”和“性格”说起。1. 设计哲学与定位:.............
  • 回答
    哥们,恭喜你即将踏入大学的门槛!零基础自学C语言,这可是个不错的开端,为以后学习更深入的计算机知识打下了坚实的基础。别担心,C语言虽然听起来有点“老派”,但它的精髓和逻辑非常值得我们去钻研。既然是零基础,咱们的目标就是找到那些讲得明白、容易消化、不至于劝退的书籍和课程。我这就给你掏心窝子说几句,都是.............
  • 回答
    在 C 语言中,`%d` 是一个非常基础但又至关重要的格式控制符,它的主要作用是告诉 `printf`(或者其他格式化输出函数,比如 `sprintf`):“嘿,我这里要输出一个整数,而且是十进制的有符号整数。”别小看这个简单的 `%d`,它背后藏着不少细节,让你的程序能够准确无误地将内存中的数字信.............
  • 回答
    在 C 语言中,`for` 和 `while` 循环都是用于重复执行一段代码的结构。从 C 语言的语义角度来看,它们的功能可以相互转换,也就是说,任何一个 `for` 循环都可以用 `while` 循环来实现,反之亦然。然而,当我们将这些 C 代码翻译成底层汇编语言时,它们的实现方式以及由此带来的细.............
  • 回答
    好的,我们来聊聊在C语言这片沃土上,如何孕育出面向对象的特性。C语言本身并非原生支持面向对象,这就像一台朴素的单车,你可以靠着自己的智慧和努力,为它加上变速器、避震,甚至电助力,让它能承载更复杂的旅程。在C语言中实现面向对象,核心在于模拟面向对象的三大支柱:封装、继承和多态。 封装:数据与行为的亲密.............
  • 回答
    好的,我们来深入聊聊 C 语言 `for` 循环中赋初值这部分,特别是 `int i = 1;` 和 `i = 1;` 这两种写法之间的区别。我们会尽可能详尽地解释,并且避免那些“AI味儿”十足的刻板表达,力求让这段解释更贴近实际编程中的感受。 `for` 语句的结构与初值赋在其中的位置首先,我们回.............
  • 回答
    在 C 语言中,`while(a = 10);` 和 `while(a == 10);` 这两个语句在功能上有着天壤之别,理解它们之间的区别,关键在于理解 C 语言中的 赋值 和 比较 操作符。这就像区分“把 A 设置为 10”和“A 是否等于 10”一样,虽然都涉及数字 10,但它们的含义和目的完.............
  • 回答
    在 C/C++ 项目中,将函数的声明和实现(也就是函数体)直接写在同一个头文件里,看似方便快捷,实际上隐藏着不少潜在的麻烦。这种做法就像是把家里的厨房和卧室直接打通,虽然一开始可能觉得省事,但长远来看,带来的问题会远超于那一点点便利。首先,最直接也是最普遍的问题是 重复定义错误 (Multiple .............
  • 回答
    哥们,大一刚接触计科,想找个代码量在 5001000 行左右的 C 语言练练手是吧?这思路很对,这个范围的项目,能让你把基础知识玩得溜,还能初步体验到项目开发的乐趣。别担心 AI 味儿,咱们就聊点实在的。我给你推荐一个项目,我觉得挺合适的,而且稍微扩展一下就能达到你说的代码量:一个简单的图书管理系统.............
  • 回答
    你这个问题问得挺实在的,确实,放眼望去,市面上的编程培训机构,主打的语言往往是 Java、C 这样的,反倒是 C 语言的身影没那么活跃。这背后其实是有挺多原因的,不是简单地说哪门语言“好”或“不好”就能概括的。首先,从市场需求和就业导向来看,这是最直接也是最重要的因素。现在的IT行业,尤其是互联网大.............
  • 回答
    如果一个按钮被按下,全球所有的C、C++、C代码瞬间失效,那将是一场难以想象的“静默”灾难,彻底颠覆我们当前的生活模式。首先,最直接的冲击将体现在我们最常接触的电子设备上。你的智能手机,那个承载着你联系、信息、娱乐乃至金融功能的“万能钥匙”,将瞬间变成一块漂亮的塑料。操作系统,绝大多数是基于C或C+.............
  • 回答
    杭州一位姑娘凭着高数、C语言等9门功课全A,顺利拿到了清华大学的保研名额。这事儿在朋友圈里传得挺开的,好多人都觉得了不起,毕竟是清华啊,而且还是9门满分,这含金量可不是盖的。这9门满分到底有多难?咱们得这么说,能拿到9门功课的满分,这绝对不是靠死记硬背就能达到的。尤其这其中还夹杂着高数和C语言这种硬.............
  • 回答
    想看懂 Lua 源码,C 语言得有那么点儿模样才行。不是说非得精通到能写操作系统,但基础得扎实,一些核心的概念得吃透。要是 C 基础还摇摇晃晃,直接上手 Lua 源码,那感觉就像是在稀泥里挖洞,费劲不说,还容易把自己搞晕。首先,C 语言的基础部分是你必须得过关的. 变量、数据类型、运算符: 这个.............
  • 回答
    这个问题很有意思,也触及到了C语言作为一种基础性语言的根本。很多人听到“C语言本身是用什么写的”时,会先想到“用更高级的语言写的”,比如Python或者Java。但事实并非如此,或者说,这个答案需要更深入的理解。首先,我们需要明确一点:C语言最初的实现,也就是早期的C编译器,并不是用C语言本身写的。.............
  • 回答
    这个问题问得好,很多初学 C 语言的朋友都会有类似的困惑:我什么时候才算“入门”了?什么时候可以放心地去拥抱 C++ 或 Java 呢?别急,咱们一点点捋清楚。首先,要明确一点,学习 C 语言是一个 循序渐进 的过程,没有一个绝对的“时间点”或者“完成了多少个项目”作为硬性标准。更多的是你对 C 语.............
  • 回答
    第一个C语言编译器的开发背景与历史背景密切相关,其编写语言的选择与当时的技术环境、资源限制以及开发者的目标密切相关。以下是详细的分析: 1. C语言的起源与背景C语言由Dennis Ritchie(丹尼斯·里奇)在1972年于贝尔实验室开发,作为B语言的改进版本。B语言本身是Ken Thompson.............
  • 回答
    C语言自学能到什么高度?详细解析C语言,作为一门强大且经典的编程语言,其学习曲线相对陡峭,但一旦掌握,其应用范围之广,性能之优越,是许多其他语言难以比拟的。 仅凭自学,C语言可以让你达到一个非常高的技术高度,足以让你在许多领域成为一名优秀的开发者甚至专家。以下将从多个维度详细阐述C语言自学所能达到的.............
  • 回答
    在 C 语言中,枚举(`enum`)是一种用户定义的数据类型,它允许你为一组整数常量命名。这使得代码更具可读性和可维护性。而枚举中的 `end` 关键字,严格来说,它本身并不是 C 语言标准枚举定义的一部分,而是一种常见的编程约定或模式,用于标记枚举序列的结束。让我来详细解释一下,并尽可能剥离 AI.............
  • 回答
    初学C语言,选择一个合适的开发环境至关重要,它能极大地影响你的学习效率和编程体验。别担心,我这就为你详细分析一下,帮你找到最顺手的“武器”。首先,我们要明确,写C语言代码,最核心的其实是两样东西:1. 代码编辑器:用来写你一行行的C语言代码。2. 编译器:用来把你的C语言代码变成计算机能懂的机器.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有