strlen

Table of Contents

1. strlen

1.1. faster strlen

正常的 strlen 需要针对每一个 byte 去 load & check null, 优化后版本大致是这样的:

#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>

/* 检测 int w 是否包含 `\0`, 例如 0xab00cdef */
unsigned int check_null(int w) {
    unsigned int mask = 0x7f7f7f7f;
    return ~(((w & mask) + mask) | mask);
}

int strlen_fast(char* start) {
    char* data = start;

    /* 跳过开头不对齐的部分, 因为 load word 时不对齐可能失败 (MIPS) 或性能下降
     * (x86, riscv) */
    while ((int)data & (sizeof(int) - 1)) {
        /* mis-aligned */
        if (*data == 0) {
            return data - start;
        }
        data++;
    }

    /* 以 word 为单位检查是否有 `\0`, 所以理论上有 4x 的性能提升 */
    while (!check_null(*((int*)(data)))) {
        data += 4;
    }

    /* 最后检查一下 `\0` 具体在 word 的哪个位置 */
    if (data[0] == 0) {
        return data - start;
    }
    if (data[1] == 0) {
        return data - start + 1;
    }
    if (data[2] == 0) {
        return data - start + 2;
    }
    if (data[3] == 0) {
        return data - start + 3;
    }
    /* unreachable */
}

int strlen_slow(char* start) {
    char* data = start;
    while (*(data++) != 0)
        ;
    return data - start - 1;
}

char* get_string(int n) {
    char ret[n + 1];
    memset(ret, 'a', n);
    ret[n] = 0;
    return strdup(ret);
}

#define timeit(prog)                                             \
    do {                                                         \
        struct timeval t1, t2;                                   \
        gettimeofday(&t1, 0);                                    \
        prog;                                                    \
        gettimeofday(&t2, 0);                                    \
        double elapsedTime = (t2.tv_sec - t1.tv_sec) * 1000.0 +  \
                             (t2.tv_usec - t1.tv_usec) / 1000.0; \
        printf("%f ms.\n", elapsedTime);                         \
    } while (0);

#define N 10000
int main(int argc, char* argv[]) {
    char* strings[N];
    for (int i = 0; i < N; i++) {
        strings[i] = get_string(i);
    }

    timeit(for (int i = 0; i < N; i++) { assert(strlen(strings[i]) == i); });

    timeit(
        for (int i = 0; i < N; i++) { assert(strlen_fast(strings[i]) == i); });

    timeit(
        for (int i = 0; i < N; i++) { assert(strlen_slow(strings[i]) == i); });
    return 0;
}
3.688000 ms.
8.572000 ms.
20.297000 ms.

1.2. check_null

check_null 用来检查一个 int 是否包含 0, 例如 0xab00cdef, 0x00abcdef 这种情况.

unsigned int check_null(int w) {
    unsigned int mask = 0x7f7f7f7f;
    return ~(((w & mask) + mask) | mask);
}

大致的思想是:

  1. 设置 mask 为 0x7f7f7f7f, 即每个 byte 为 b01111111, 最高位为 0.
  2. 假设 data 对应的某一个 byte 不是 0 (低 7 位包含 1)
  3. 计算 w = data & mask, 因为 data 包含 1, 所以 w 包含 1
  4. 计算 w = w + mask, 因为 w 低 7 位包含 1, 所以 w + mask 会产生进位, 导致 w 最高位变为 1
  5. 计算 w = ~(w|mask), 结果 w 为 0

所以 check_null 返回 0 表示一个 int 不包含 0

1.3. GCC O2 的一个 `bug`?

在实现上碰到一个问题:

当 check_null 中的类型使用 int (而不是 unsigned int) 时, 用 `-O2` 编译时, check_null 看起来被直接忽略掉了…

原因可能是: gcc 进行代码优化时认为 check_null 结果不可能是 0. 因为计算 int 加法 `((w & mask) + mask)` 时,理论上结果不可能是负数, 即最高位不可能为 1, 所以 (((w & mask) + mask) | mask) 不可能全 1, 所以最终结果不可能为 0.

实际上, 涉及 bitwise 操作时, 最好用 unsigned.

这个问题的 root cause 参考 bitwise constant propagation

Backlinks

Retargeting GCC To RISC-V (Retargeting GCC To RISC-V > newlib/glibc > 某些可以优化的函数): - strlen

bitwise constant propagation (GCC Constant Propagation > bitwise constant propagation > example): 以 strlen 为例,

Author: [email protected]
Date: 2022-03-01 Tue 11:19
Last updated: 2022-04-14 Thu 19:56

知识共享许可协议