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?补充一个,
XOR linked list原理很简单,利用C的按位异或只用一个字节的指针信息就实现双向链表
除了开头和结尾,每个节点保存其相邻节点的地址的异或结果,正向遍历时用当前节点地址字段中保存的值异或后一个,反向遍历时异或前一个。
而开头节点存下一个节点地址,尾节点保存前一节点地址。
这样一来,用一个接口就能实现链表双向遍历,还比双链表节省空间
自定义"控制流"
假设我们要打开一个文件读写, 读写完毕把文件关闭, 并且加一点错误处理, 那么代码会像这样:
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 的技术,详见 https://github.com/luikore/ccut/blob/master/ccut.c
"类型推断"
我们知道 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 ... }
其他的, 相信看这个问答更有用:
有很多啊
快速范围判断
经常要批量判断某些值在不在范围内,如果 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 的方案。
。。。。
待续
本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度,google,bing,sogou 等
© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有