如何在编译时进行短字符串优化?

How can I do short string optimization at compile time?

提问人:user16217248 提问时间:10/15/2023 最后编辑:John Kugelmanuser16217248 更新时间:10/17/2023 访问量:127

问:

我有一个基本的字符串结构:

typedef struct String {
    size_t size;
    union {
        char *ptr, buf[sizeof(char *)];
    };
} String;

这个想法是能够从类似的东西中提取一个:char *String

extern String string;
char *str = string.size >= sizeof(char *) ? string.ptr : string.buf;

我有一个问题。如果我有一个必须在编译时初始化的 /global 结构怎么办?如果我不想对给定大小的假设进行硬编码,该怎么办?conststaticStringchar *

static const String string = {
    sizeof(CONSTANT_STRING)-1,
#if sizeof(CONSTANT_STRING) > sizeof(char *)
    .name
#else
    .buf
#endif
        = CONSTANT_STRING

但正如我们所知,sizeof() 发生在预处理时间之后,因此不能在 #if 中使用

如何在编译时有条件地将字符串的一个成员或另一个成员分配给给定值,以便生成的表达式是编译时常量?

c 字符串 联合 编译 时常量

评论


答:

2赞 user16217248 10/15/2023 #1

免责声明:此答案需要 C23 才能可靠工作

为了使复合文字成为保证编译时常量,必须将它们指定为 C23 特定的功能。否则,如果没有它,可能会也可能不会起作用,这取决于编译器及其对标准的解释。constexprconstexpr


这是可以做到的,但我不得不重新设计结构。我可以有一个命名的联合成员,并使用运算符有条件地将其初始化为不同的复合文字,而不是使用未命名的嵌套联合:String?:

typedef union Data {
    char *ptr, buf[sizeof(char *)];
} Data;
typedef struct String {
    size_t size;
    Data data;
} String;
#define CONSTANT_STRING "Hello"
static constexpr String string = {
    .size = sizeof(CONSTANT_STRING)-1,
    .data = sizeof(CONSTANT_STRING) > sizeof(char *)
        ? (constexpr Data){.ptr = CONSTANT_STRING}
        : (constexpr Data){.buf = CONSTANT_STRING}
};
1赞 Myst 10/15/2023 #2

我想补充一点,空间比您在 中实际使用的空间要多,因为 1-8 之间的数字值不需要类型......structsize_t

在 64 位系统上使用时,以下不完整的示例将为小字符串优化增加 7 个字节的空间:

typedef struct my_string_s {
   unsigned char len[sizeof(size_t)];
   char * ptr;
} my_string_s;

这里有一个“野生”的例子。

facil.io C STL String 模块具有更好的优化,但这来自 CLI 模块,它显示了这种方法的工作原理。它专为 64 位系统设计,可能会在 32 位系统上增加额外的 4 字节开销(但这 4 个字节将由短字符串使用):

/* Note: requires some macros and includes from the facil.io C STL core header */

/** string container */
typedef struct {
  uint8_t em;     /* embedded? const? how long? */
  uint8_t pad[3]; /* padding - embedded buffer starts here */
  uint32_t len;   /* if not embedded, otherwise see `em` */
  char *str;      /* if not embedded, otherwise see `pad` */
} fio_cli_str_s;

/* cli string free / destroy by context */
FIO_SFUNC void fio_cli_str_destroy(fio_cli_str_s *s) {
  if (!s || s->em || !s->str)
    return;
  FIO_LEAK_COUNTER_ON_FREE(fio_cli_str);
  FIO_MEM_FREE_(s->str, s->len);
  *s = (fio_cli_str_s){0};
}

/* cli string info */
FIO_IFUNC fio_buf_info_s fio_cli_str_buf(fio_cli_str_s *s) {
  fio_buf_info_s r = {0};
  if (s && (s->em || s->len))
    r = ((s->em) & 127) ? (FIO_BUF_INFO2((char *)s->pad, (size_t)s->em))
                        : (FIO_BUF_INFO2(s->str, (size_t)s->len));
  return r;
}

/* cli string copy */
FIO_SFUNC fio_cli_str_s fio_cli_str_copy(fio_buf_info_s s) {
  fio_cli_str_s r = {0};
  if (s.len < sizeof(r) - 2) {
    r.em = s.len;
    FIO_MEMCPY(r.pad, s.buf, s.len);
    return r;
  }
  r.len = (uint32_t)s.len;
  r.str = (char *)FIO_MEM_REALLOC_(NULL, 0, s.len + 1, 0);
  FIO_ASSERT_ALLOC(r.str);
  FIO_LEAK_COUNTER_ON_ALLOC(fio_cli_str);
  FIO_MEMCPY(r.str, s.buf, s.len);
  r.str[r.len] = 0;
  return r;
}

/* cli string tmp copy */
FIO_SFUNC fio_cli_str_s fio_cli_str(fio_buf_info_s s) {
  fio_cli_str_s r = {0};
  if (s.len < sizeof(r) - 2) {
    r.em = s.len;
    FIO_MEMCPY(r.pad, s.buf, s.len);
    return r;
  }
  r.em = 128; /* mark as const, memory shouldn't be freed */
  r.len = (uint32_t)s.len;
  r.str = s.buf;
  return r;
}

评论

0赞 user16217248 10/16/2023
我不太确定这将如何工作。存储 和 之间的任何值,但如果该值恰好小于 ,则程序会将 解释为嵌入的字符串,否则解释为指向其他位置的指针。我能想到的唯一方法是将其中一个位用作标志。size_t0SIZE_MAXsizeof(char *)unionsize_t
0赞 Myst 10/23/2023
它可以通过使字符串的最大支持长度小得多来工作。例如,在 32 位计算机上,您可能只有 24 位的长度,而在 64 位的机器上只有 56 位...这允许您对标志和长度数据使用整体(8 位)。您可能只需要 5 位:1 位用于嵌入长度,4 位用于嵌入长度......但是这 3 个额外的位可能不值得额外的代码。charconst
0赞 Myst 10/23/2023
仅供参考:24 位可以容纳 16Mb 的字符串,这可能比编译时常量所需的要多。