C语言char*指针相减运算及strlen缓存实现原理疑问
指针减法运算逻辑解答
你问的size_t len = end - str;是C语言标准定义的合法指针运算,规则非常明确:
- 只有两个指向同一块连续内存的同类型指针才能做减法,运算结果不是地址的字节差值,而是两个指针之间间隔的对应类型元素的个数。因为这里是
char*类型,每个char占1字节,所以结果刚好等于两个地址的字节偏移量。 - 这个运算和数组下标规则是完全同源的:C语言里数组的下标访问本质就是指针偏移,
arr[i]和*(arr + i)是完全等价的;反过来同一块数组(连续内存)里的两个指针相减,得到的就是两个位置的下标差值。
举个实际的例子:如果缓存的长字符串起始地址是0x1000,总长度是100000,那么end = str + len的值就是0x1000 + 100000,刚好指向字符串末尾的\0结束符。如果后续传入的str是0x10200(也就是原字符串偏移512字节的位置),那end - str算出来就是99488,正好是从当前传入位置到字符串末尾的长度,完全不需要逐字节遍历找结束符。
注意:非同一连续内存块的指针做减法属于未定义行为,代码里前置的
str >= start && str <= end判断,就是为了保证参与减法的两个指针都落在之前缓存的长字符串内存范围内,从根源上规避非法运算。
strlen缓存优化实现原理
这个优化针对的是非常特定的场景:当时GTA Online启动时会加载一个体积超大的JSON配置文件,加载过程中会成百上千次对这个JSON字符串的不同位置调用strlen。而原生strlen的逻辑是从传入的指针位置开始逐字节往后遍历,直到找到\0结束符才返回长度,每次调用都是O(n)的时间复杂度,字符串越长、调用次数越多,浪费的CPU时间就越夸张,这也是当时加载慢的核心瓶颈之一。
整套缓存逻辑把函数分成了两条路径,完整代码如下:
size_t strlen_cacher(char *str) { static char *start; static char *end; size_t len; const size_t cap = 20000; // 缓存命中判断 if (start && str >= start && str <= end) { len = end - str; // 剩余长度不足阈值时关闭hook,避免误判其他内存 if (len < cap / 2) MH_DisableHook((LPVOID)strlen_addr); // O(1)快速返回 return len; } // 缓存未命中,走原生strlen逻辑 len = builtin_strlen(str); // 遇到长度超过阈值的超长字符串,记录起止地址到缓存 if (len > cap) { start = str; end = str + len; } // 常规O(n)返回 return len; }
具体执行逻辑:
- 用静态变量
start和end做缓存,第一次遇到长度超过20000的超长字符串时,记录下这个字符串的起始地址、结束地址(结束地址=起始地址+总长度,刚好指向末尾的\0)。 - 后续每次调用
strlen时,先判断当前传入的指针是不是落在缓存的[start, end]区间内:- 如果在区间内,直接用指针减法算出剩余长度,直接返回,全程不需要遍历内存,时间复杂度O(1),比原生
strlen快几个数量级。 - 如果算出来的剩余长度已经小于10000(cap的一半),说明已经快遍历到这个长字符串的末尾了,后续不会再对这个字符串频繁调用
strlen,就直接卸载hook回到原生strlen逻辑,避免后续把其他内存块的指针误判成属于这个缓存字符串,引发计算错误。
- 如果在区间内,直接用指针减法算出剩余长度,直接返回,全程不需要遍历内存,时间复杂度O(1),比原生
- 如果当前指针不在缓存区间内,就调用原生
strlen正常计算长度,要是碰到新的超长字符串就更新缓存,否则直接返回结果。
内容的提问来源于stack exchange,提问作者qwertyman9938
相关产品推荐
相关产品推荐

