#string #redis #dynamic #lib #printf #length #order

nightly sds

Rust语言的Simple Dynamic Strings库包装,由Redis创建并维护

1个不稳定版本

使用旧的Rust 2015

0.1.0 2018年7月15日

#1657数据库接口

MIT 许可证

93KB
1.5K SLoC

C 1K SLoC // 0.2% comments Rust 309 SLoC // 0.1% comments

简单动态字符串

关于版本2的说明:这是SDS的一个更新版本,旨在最终统一Redis、Disque、Hiredis和独立的SDS版本。这个版本与SDS版本1*不二进制兼容,但API有99%的兼容性,因此切换到新库应该是微不足道的。

请注意,这个版本的SDS在某些工作负载中可能较慢,但与V1相比,它使用更少的内存,因为头大小是动态的,并依赖于分配的字符串。

此外,它还包括一些额外的API函数,特别是sdscatfmt,这是sdscatprintf的一个更快的版本,可以用于更简单的案例,以避免libc printf家族函数的性能惩罚。

SDS字符串的工作原理

SDS是针对C语言设计的字符串库,旨在通过添加堆分配的字符串来增强有限的libc字符串处理功能。

  • 更易于使用。
  • 二进制安全。
  • 计算效率更高。
  • 但仍然... 与正常C字符串函数兼容。

这是通过一种替代设计实现的,其中我们不是使用C结构来表示字符串,而是使用一个二进制前缀,该前缀存储在SDS返回给用户的实际字符串指针之前。

+--------+-------------------------------+-----------+
| Header | Binary safe C alike string... | Null term |
+--------+-------------------------------+-----------+
         |
         `-> Pointer returned to the user.

由于在返回的实际指针之前作为前缀存储了元数据,并且由于每个SDS字符串在字符串的实际内容之后隐式地添加了一个空终止符,因此SDS字符串与C字符串很好地协同工作,并且用户可以自由地与其他标准C字符串函数互换使用,这些函数以只读方式访问字符串。

SDS是我过去为我的日常C编程需求开发的一个C字符串,后来它被移入Redis,在那里它被广泛使用,并被修改以适用于高性能操作。现在它已从Redis提取出来,作为一个独立的工程分叉。

由于SDS在Redis内部存在多年,它不仅提供了C语言中简单字符串操作的高级函数,还提供了一套低级函数,这使得编写高性能代码成为可能,而不必为使用高级字符串库付出代价。

SDS的优缺点

通常,C语言的动态字符串库实现使用一个定义字符串的结构。该结构有一个由字符串函数管理的指针字段,因此看起来像这样

struct yourAverageStringLibrary {
    char *buf;
    size_t len;
    ... possibly more fields here ...
};

如前所述,SDS字符串不遵循此模式,而是使用一个单独的分配,其中前缀位于返回给字符串的实际地址之前。

与传统方法相比,这种方法既有优点也有缺点

缺点 #1:许多函数将新字符串作为值返回,因为有时SDS需要创建一个带有更多空间的字符串,所以大多数SDS API调用看起来像这样

s = sdscat(s,"Some more data");

如您所见,s被用作sdscat的输入,但也被设置为SDS API调用的返回值,因为我们不确定该调用是否修改了我们传递的SDS字符串或分配了新的字符串。忘记将sdscat或类似函数的返回值赋回持有SDS字符串的变量会导致错误。

缺点 #2:如果SDS字符串在程序的不同位置共享,则修改字符串时必须修改所有引用。然而,大多数时候,当你需要共享SDS字符串时,最好将它们封装到具有reference count的结构中,否则很容易发生内存泄漏。

优点 #1:你可以将SDS字符串传递给为C函数设计的函数,而无需访问结构成员或调用函数,如下所示

printf("%s\n", sds_string);

在其他大多数库中,这将是如下所示

printf("%s\n", string->buf);

或者

printf("%s\n", getStringPointer(string));

优点 #2:访问单个字符非常简单。C是一种底层语言,因此这在许多程序中是一个重要的操作。使用SDS字符串访问单个字符非常自然

printf("%c %c\n", s[0], s[1]);

在其他库中,你的最佳选择是将string->buf(或调用函数以获取字符串指针)分配给一个char指针并使用它。然而,由于其他库可能在每次调用可能修改字符串的函数时隐式地重新分配缓冲区,你必须再次获取缓冲区的引用。

优点 #3:单个分配具有更好的缓存局部性。通常,当你使用结构表示字符串并使用字符串库创建字符串时,你会有两个不同的分配,分别代表字符串的结构和实际的字符串缓冲区。随着时间的推移,缓冲区被重新分配,并且它很可能位于与结构本身完全不同的内存部分。由于现代程序的性能通常由缓存缺失主导,SDS可能在许多工作负载中表现更好。

SDS基础

SDS字符串的类型只是char指针char *。然而,SDS在其头文件中将sds定义为char *的别名:你应该使用sds类型以确保记住程序中的给定变量持有SDS字符串而不是C字符串,但这不是强制性的。

这是你可以编写的最简单的SDS程序,它执行了一些操作

sds mystring = sdsnew("Hello World!");
printf("%s\n", mystring);
sdsfree(mystring);

output> Hello World!

上述小程序已经展示了关于SDS的一些重要内容

  • SDS字符串通过sdsnew()函数或我们稍后将看到的其他类似函数创建和堆分配。
  • SDS字符串可以像任何其他C字符串一样传递给printf()
  • SDS字符串需要使用sdsfree进行释放,因为它们是在堆上分配的。

创建SDS字符串

sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);

创建SDS字符串有许多方法

  • sdsnew函数从一个C语言中以null终止的字符串创建SDS字符串。我们已经在上面的例子中看到了它是如何工作的。

  • sdsnewlen函数与sdsnew类似,但它不是假设输入字符串是null终止的来创建字符串,而是获取一个额外的长度参数。这样您就可以使用二进制数据创建字符串

    char buf[3];
    sds mystring;
    
    buf[0] = 'A';
    buf[1] = 'B';
    buf[2] = 'C';
    mystring = sdsnewlen(buf,3);
    printf("%s of len %d\n", mystring, (int) sdslen(mystring));
    
    output> ABC of len 3
    

    注意:sdslen返回值被转换为int,因为它返回一个size_t类型。您可以使用正确的printf说明符而不是进行转换。

  • sdsempty函数创建一个空的长为零的字符串

    sds mystring = sdsempty();
    printf("%d\n", (int) sdslen(mystring));
    
    output> 0
    
  • sdsdup函数复制一个已经存在的SDS字符串

    sds s1, s2;
    
    s1 = sdsnew("Hello");
    s2 = sdsdup(s1);
    printf("%s %s\n", s1, s2);
    
    output> Hello Hello
    

获取字符串长度

size_t sdslen(const sds s);

在上面的例子中,我们已经使用sdslen函数来获取字符串的长度。这个函数的工作方式与libc中的strlen类似,但

  • 它在常数时间内运行,因为长度存储在SDS字符串的前缀中,所以调用sdslen并不昂贵,即使是在调用非常长的字符串时也是如此。
  • 该函数与任何其他SDS字符串函数一样是二进制安全的,因此长度是字符串的真正长度,而不管内容如何,如果字符串中包含中间的null终止字符,则没有问题。

作为SDS字符串二进制安全性的一个示例,我们可以运行以下代码

sds s = sdsnewlen("A\0\0B",4);
printf("%d\n", (int) sdslen(s));

output> 4

注意,SDS字符串始终在末尾以null终止,所以即使在这种情况下s[4]也将是null终止符,但是使用printf打印字符串将只打印"A",因为libc将SDS字符串视为正常的C字符串。

销毁字符串

void sdsfree(sds s);

要销毁SDS字符串,只需用字符串指针调用sdsfree。注意,即使使用sdsempty创建的空字符串也需要销毁,否则会导致内存泄漏。

sdsfree函数在传递的不是SDS字符串指针而是NULL时不会执行任何操作,因此您在调用它之前不需要显式检查NULL

if (string) sdsfree(string); /* Not needed. */
sdsfree(string); /* Same effect but simpler. */

连接字符串

将字符串连接到其他字符串可能是您将最频繁使用动态C字符串库的操作。SDS提供了不同的函数来将字符串连接到现有字符串。

sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);

主要的字符串连接函数是sdscatlensdscat,它们是相同的,唯一的区别是sdscat没有显式的长度参数,因为它期望一个null终止的字符串。

sds s = sdsempty();
s = sdscat(s, "Hello ");
s = sdscat(s, "World!");
printf("%s\n", s);

output> Hello World!

有时您想将SDS字符串连接到另一个SDS字符串,因此您不需要指定长度,但字符串不需要以null终止,而可以包含任何二进制数据。为此有一个特殊函数

sds sdscatsds(sds s, const sds t);

用法简单

sds s1 = sdsnew("aaa");
sds s2 = sdsnew("bbb");
s1 = sdscatsds(s1,s2);
sdsfree(s2);
printf("%s\n", s1);

output> aaabbb

有时你不想向字符串追加任何特殊数据,但你要确保整个字符串至少包含给定数量的字节。

sds sdsgrowzero(sds s, size_t len);

sdsgrowzero 函数如果当前字符串长度已经是 len 字节,则不会做任何操作,否则它会将字符串扩展到 len 字节,并在其中填充零字节。

sds s = sdsnew("Hello");
s = sdsgrowzero(s,6);
s[5] = '!'; /* We are sure this is safe because of sdsgrowzero() */
printf("%s\n', s);

output> Hello!

字符串格式化

存在一个特殊的字符串连接函数,它接受类似 printf 的格式说明符,并将格式化后的字符串连接到指定的字符串。

sds sdscatprintf(sds s, const char *fmt, ...) {

示例

sds s;
int a = 10, b = 20;
s = sdsnew("The sum is: ");
s = sdscatprintf(s,"%d+%d = %d",a,b,a+b);

通常你需要直接从 printf 格式说明符创建 SDS 字符串。因为 sdscatprintf 实际上是一个连接字符串的函数,你只需要将你的字符串连接到一个空字符串。

char *name = "Anna";
int loc = 2500;
sds s;
s = sdscatprintf(sdsempty(), "%s wrote %d lines of LISP\n", name, loc);

你可以使用 sdscatprintf 将数字转换为 SDS 字符串。

int some_integer = 100;
sds num = sdscatprintf(sdsempty(),"%d\n", some_integer);

但是这很慢,我们有一个特殊的函数来提高效率。

快速数字到字符串操作

在某种类型的程序中,从整数创建 SDS 字符串可能是一个常见的操作,虽然你可以使用 sdscatprintf 来这样做,但性能损失很大,所以 SDS 提供了一个专门的函数。

sds sdsfromlonglong(long long value);

用法如下

sds s = sdsfromlonglong(10000);
printf("%d\n", (int) sdslen(s));

output> 5

字符串修剪和获取范围

字符串修剪是一个常见的操作,其中从字符串的左右两侧移除一组字符。关于字符串的另一个有用操作是能够从较大的字符串中提取一个范围。

void sdstrim(sds s, const char *cset);
void sdsrange(sds s, int start, int end);

SDS 提供了这两个操作,分别是通过 sdstrimsdsrange 函数。但是请注意,这两个函数与大多数修改 SDS 字符串的函数的工作方式不同,因为它们的返回值是空的:基本上,这些函数总是破坏性地修改传入的 SDS 字符串,而不会分配一个新的字符串,因为修剪和范围永远不会需要更多空间:这些操作只能从原始字符串中删除字符。

由于这种行为,这两个函数都很快,不涉及重新分配。

以下是一个字符串修剪示例,其中从 SDS 字符串中移除了换行符和空格。

sds s = sdsnew("         my string\n\n  ");
sdstrim(s," \n");
printf("-%s-\n",s);

output> -my string-

基本上,sdstrim 以要修剪的 SDS 字符串作为第一个参数,以及一个以 null 结尾的字符集,从字符串的左右两侧移除这些字符。只要它们没有被不在修剪字符列表中的字符中断,就会移除这些字符:这就是为什么在上面的示例中,"my""string" 之间的空间被保留。

获取范围与修剪类似,但它不是获取一组字符,而是获取两个索引,分别表示字符串中指定的起始和结束位置(由零基索引指定),以获取要保留的范围。

sds s = sdsnew("Hello World!");
sdsrange(s,1,4);
printf("-%s-\n");

output> -ello-

索引可以是负数,以指定从字符串末尾开始的起始位置,因此 -1 表示最后一个字符,-2 表示倒数第二个,依此类推。

sds s = sdsnew("Hello World!");
sdsrange(s,6,-1);
printf("-%s-\n");
sdsrange(s,0,-2);
printf("-%s-\n");

output> -World!-
output> -World-

sdsrange 在实现处理协议或发送消息的网络服务器时非常有用。例如,以下代码用于在 Redis 集群节点之间实现消息总线上的写入处理器。

void clusterWriteHandler(..., int fd, void *privdata, ...) {
    clusterLink *link = (clusterLink*) privdata;
    ssize_t nwritten = write(fd, link->sndbuf, sdslen(link->sndbuf));
    if (nwritten <= 0) {
        /* Error handling... */
    }
    sdsrange(link->sndbuf,nwritten,-1);
    ... more code here ...
}

每当我们要发送消息的节点的套接字可写时,我们尝试写入尽可能多的字节,并使用 sdsrange 从缓冲区中删除已发送的内容。

将新消息排队到集群中某个节点上的函数将简单地使用 sdscatlen 将更多数据放入发送缓冲区。

请注意,Redis集群总线实现了一种二进制协议,但由于SDS是二进制安全的,这并不成问题,因此SDS的目标不仅仅是为C程序员提供一个高级字符串API,还包括易于管理的动态分配缓冲区。

字符串复制

标准C库中最危险和臭名昭著的函数可能是strcpy,因此,在更精心设计的动态字符串库的背景下,字符串复制的概念几乎无关紧要。通常你所做的就是创建包含所需内容的字符串,或者根据需要附加更多内容。

然而,SDS提供了一个字符串复制函数,在性能关键代码段中非常有用,但是我认为其实际用途有限,因为该函数从未在Redis代码库的50,000行代码中调用过。

sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);

SDS的字符串复制函数称为sdscpylen,其工作方式如下

s = sdsnew("Hello World!");
s = sdscpylen(s,"Hello Superman!",15);

如您所见,该函数接收SDS字符串s作为输入,但也返回一个SDS字符串。这类似于许多修改字符串的SDS函数:这样返回的SDS字符串可能是原始的已修改字符串,也可能是新分配的一个(例如,如果旧SDS字符串中没有足够的空间)。

sdscpylen将简单地用您通过指针和长度参数传递的新数据替换旧SDS字符串中的内容。还有一个名为sdscpy的类似函数,它不需要长度,而是期望一个以null终止的字符串。

您可能会 wonder,为什么SDS库中需要一个字符串复制函数,因为您可以从头开始创建一个新的SDS字符串,而不是在现有的SDS字符串中复制值。原因是效率:sdsnewlen始终会分配一个新的字符串,而sdscpylen将尝试重用现有的字符串,如果用户指定的新内容有足够的空间,否则将只分配一个新字符串。

引用字符串

为了向程序用户提供一致的结果,或者为了调试目的,通常很重要将可能包含二进制数据或特殊字符的字符串转换为引用字符串。在这里,我们所说的引用字符串是编程源代码中字符串字面量的常用格式。然而,今天这种格式也是众所周知的序列化格式(如JSON和CSV)的一部分,因此它已经超越了仅表示程序源代码中的字符串字面量的简单目标。

以下是一个引用字符串字面量的示例

"\x00Hello World\n"

第一个字节是一个零字节,最后一个字节是一个换行符,因此字符串中有两个非字母数字字符。

SDS使用一个连接函数来实现此目标,该函数将输入字符串的引用字符串表示连接到现有字符串。

sds sdscatrepr(sds s, const char *p, size_t len);

scscatrepr(其中repr表示“表示”)遵循通常的SDS字符串函数规则,接受字符指针和长度,因此您可以使用它与SDS字符串,使用strlen()作为len参数的正常C字符串,或二进制数据。以下是一个示例用法

sds s1 = sdsnew("abcd");
sds s2 = sdsempty();
s[1] = 1;
s[2] = 2;
s[3] = '\n';
s2 = sdscatrepr(s2,s1,sdslen(s1));
printf("%s\n", s2);

output> "a\x01\x02\n"

sdscatrepr使用的转换规则如下

  • \"使用反斜杠进行引用。
  • 它引用了特殊字符 '\n''\r''\t''\a''\b'
  • 所有其他非打印字符,如果未通过 isprint 测试,则将以其 \x.. 形式引用,即:反斜杠后跟 x,然后是表示字符字节值的两位十六进制数。
  • 该函数始终添加初始和最后的双引号字符。

有一个SDS函数能够执行反向转换,并在下文的 Tokenization(分词)部分有文档说明。

分词

分词是将较大的字符串分割成较小字符串的过程。在这个特定例子中,分割操作是通过指定另一个作为分隔符的字符串来执行的。例如,在下面的字符串中,有两个子字符串由 |-| 分隔符分隔

foo|-|bar|-|zap

一个更常见的分隔符是一个字符

foo,bar,zap

在许多程序中,为了获取由行组成的子字符串,处理一行是有用的,因此SDS提供了一个函数,该函数在给定的字符串和分隔符的情况下返回一个SDS字符串数组。

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
void sdsfreesplitres(sds *tokens, int count);

通常,该函数可以同时处理SDS字符串或常规C字符串。前两个参数 slen 指定了要分词的字符串,其他两个参数 sepseplen 指定了在分词过程中使用的分隔符。最后一个参数 count 是一个指向整数的指针,它将被设置为返回的标记(子字符串)数量。

返回值是一个堆分配的SDS字符串数组。

sds *tokens;
int count, j;

sds line = sdsnew("Hello World!");
tokens = sdssplitlen(line,sdslen(line)," ",1,&count);

for (j = 0; j < count; j++)
    printf("%s\n", tokens[j]);
sdsfreesplitres(tokens,count);

output> Hello
output> World!

返回的数组是堆分配的,数组的单个元素是常规的SDS字符串。您可以通过调用 sdsfreesplitres(如示例所示)来释放一切。或者,您可以自由使用 free 函数释放数组,并按常规使用和/或释放单个SDS字符串。

一个有效的方法是将以某种方式重新使用的数组元素设置为 NULL,并使用 sdsfreesplitres 释放所有其余内容。

面向命令行的分词

通过分隔符进行分割是一个有用的操作,但通常不足以执行涉及一些非平凡字符串操作的最常见任务之一,即实现程序的 命令行界面

这就是为什么SDS还提供了一个额外的函数,允许您通过键盘以交互式方式或通过文件、网络或任何其他方式提供用户参数,并将其分割成标记。

sds *sdssplitargs(const char *line, int *argc);

sdssplitargs 函数返回的SDS字符串数组与 sdssplitlen 相同。释放结果的功能也是相同的,是 sdsfreesplitres。区别在于分词的方式。

例如,如果输入是以下行

call "Sabrina"    and "Mark Smith\n"

该函数将返回以下标记

  • "call"
  • "Sabrina"
  • "and"
  • "Mark Smith\n"

基本上,不同的标记需要用一个或多个空格隔开,每个标记也可以是一个以相同格式引用的字符串,就像 sdscatrepr 能够输出的那样。

字符串连接

有两个函数通过将字符串连接成一个单一的字符串来执行与标记化相反的操作。

sds sdsjoin(char **argv, int argc, char *sep, size_t seplen);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);

这两个函数接受一个字符串数组作为输入,长度为 argc,以及一个分隔符及其长度,并生成一个输出为所有指定字符串由指定分隔符分隔的SDS字符串。

sdsjoinsdsjoinsds 的区别在于前者接受C空终止字符串作为输入,而后者要求数组中的所有字符串都是SDS字符串。然而,正因为如此,只有 sdsjoinsds 能够处理二进制数据。

char *tokens[3] = {"foo","bar","zap"};
sds s = sdsjoin(tokens,3,"|",1);
printf("%s\n", s);

output> foo|bar|zap

错误处理

所有返回SDS指针的SDS函数在内存不足时也可能返回 NULL,这基本上是你需要执行的唯一检查。

然而,许多现代C程序简单地通过中止程序来处理内存不足的情况,因此你可能也想这样做,通过直接包装 malloc 和其他相关内存分配调用。

SDS内部和高级用法

在这份文档的开头,解释了SDS字符串是如何分配的。然而,返回给用户的指针之前存储的前缀被分类为头部,但没有进一步的细节。对于高级用法,更好地深入了解SDS的内部结构,并展示实现它的结构。

struct sdshdr {
    int len;
    int free;
    char buf[];
};

正如你所看到的,这个结构可能类似于传统字符串库的结构,然而结构的 buf 字段是不同的,因为它不是一个指针,而是一个没有声明的长度的数组。所以 buf 实际上指向 free 整数之后的第一个字节。因此,为了创建一个SDS字符串,我们只需分配一个与 sdshdr 结构加字符串长度一样大的内存块,再加上每个SDS字符串必须有的一个额外的空字符。

结构的 len 字段相当明显,它是SDS字符串的当前长度,每次通过SDS函数调用修改字符串时都会重新计算。而 free 字段代表当前分配中可用于存储更多字符的空闲内存量。

因此,实际的SDS布局如下所示

+------------+------------------------+-----------+---------------\
| Len | Free | H E L L O W O R L D \n | Null term |  Free space   \
+------------+------------------------+-----------+---------------\
             |
             `-> Pointer returned to the user.

你可能想知道为什么字符串末尾有一些空闲空间,这看起来像是一种浪费。实际上,在创建新的SDS字符串后,末尾根本没有空闲空间:分配将尽可能小,仅足以容纳头部、字符串和空字符。

s = sdsempty();
s = sdscat(s,"foo");
s = sdscat(s,"bar");
s = sdscat(s,"123");

由于SDS试图高效,它不能每次新数据附加时都重新分配字符串,因为这会非常低效,所以它会在每次扩大字符串时使用一些空闲空间的预分配。

使用的预分配算法如下:每次字符串重新分配以容纳更多字节时,实际分配的大小是所需最小值的两倍。例如,如果字符串当前包含30个字节,我们再连接2个字节,那么SDS不会总共分配32个字节,而是分配64个字节。

然而,预分配可以提前执行的最大分配量有一个硬限制,由 SDS_MAX_PREALLOC 定义。SDS永远不会分配超过1MB的额外空间(默认情况下,您可以更改此默认值)。

缩小字符串

sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);

有时存在一类程序,它们需要使用非常少的内存。在字符串连接、裁剪、范围操作之后,字符串可能最终在末尾留下非平凡数量的额外空间。

可以使用函数sdsRemoveFreeSpace将字符串的大小调整回其最小大小,以容纳当前内容。

s = sdsRemoveFreeSpace(s);

还有一个函数可以用来获取给定字符串的总分配大小,称为sdsAllocSize

sds s = sdsnew("Ladies and gentlemen");
s = sdscat(s,"... welcome to the C language.");
printf("%d\n", (int) sdsAllocSize(s));
s = sdsRemoveFreeSpace(s);
printf("%d\n", (int) sdsAllocSize(s));

output> 109
output> 59

注意:SDS低级API使用驼峰命名法以警告您正在玩火。

手动修改SDS字符串

void sdsupdatelen(sds s);

有时您可能想手动修改SDS字符串,而不使用SDS函数。在以下示例中,我们隐式地更改了字符串的长度,但我们希望逻辑长度反映以空字符终止的C字符串。

函数sdsupdatelen正是如此,它将指定字符串的内部长度信息更新为通过strlen获取的长度。

sds s = sdsnew("foobar");
s[2] = '\0';
printf("%d\n", sdslen(s));
sdsupdatelen(s);
printf("%d\n", sdslen(s));

output> 6
output> 2

共享SDS字符串

如果您正在编写一个程序,其中在不同数据结构之间共享相同的SDS字符串是有益的,那么绝对建议将SDS字符串封装到结构中,这些结构记住字符串的引用次数,并包含增加和减少引用次数的函数。

这种方法是一种称为引用计数的内存管理技术,在SDS的上下文中有两个优点

  • 您不太可能创建内存泄漏或由于未释放SDS字符串或释放已释放的字符串而导致的错误。
  • 当您修改SDS字符串时,您不需要更新每个引用(因为新的SDS字符串可能指向不同的内存位置)。

虽然这绝对是一个非常常见的编程技术,但我将在下面概述基本思想。您创建一个类似的结构

struct mySharedString {
    int refcount;
    sds string;
}

当创建新字符串时,结构被分配并返回,refcount设置为1。然后您有两个函数来更改共享字符串的引用计数

  • incrementStringRefCount将简单地增加结构中的refcount的1。它将在您将字符串添加到新数据结构、变量或其他内容时被调用。
  • decrementStringRefCount用于删除引用。然而,此函数是特殊的,因为当refcount降到零时,它会自动释放SDS字符串和mySharedString结构。

与堆检查器的交互

由于SDS返回指向使用malloc分配的内存块中间的指针,堆检查器可能会有问题,但是

  • 流行的Valgrind程序将检测SDS字符串是可能丢失的内存,而不是肯定丢失的,因此很容易判断是否存在泄漏。我多年来一直在使用Valgrind与Redis,每个真正的泄漏都一致地被检测为“肯定丢失”。
  • OSX仪器工具不将SDS字符串检测为泄漏,但能够正确处理指向内存块中间的指针。

从系统调用中进行零拷贝追加

到目前为止,您应该已经拥有了所有工具来通过阅读源代码深入了解SDS库,然而,您可以使用导出的低级API实现一个有趣的模式,该模式用于Redis内部以改进网络代码的性能。

使用sdsIncrLen()sdsMakeRoomFor(),您可以构建以下模式,将来自内核的字节追加到SDS字符串的末尾,而不将其复制到中间缓冲区中

oldlen = sdslen(s);
s = sdsMakeRoomFor(s, BUFFER_SIZE);
nread = read(fd, s+oldlen, BUFFER_SIZE);
... check for nread <= 0 and handle it ...
sdsIncrLen(s, nread);

sdsIncrLensds.c的源代码中有文档说明。

将SDS嵌入到您的项目中

这就像是在您的项目中复制以下文件一样简单。

  • sds.c
  • sds.h
  • sdsalloc.h

源代码很小,每个C99编译器都应该能够无问题地处理它。

为SDS使用不同的分配器

内部,sds.c使用在 sdsalloc.h 中定义的分配器。这个头文件只定义了malloc、realloc和free的宏,默认使用libc中的 malloc()realloc()free()。只需编辑此文件即可更改分配函数的名称。

使用SDS的程序可以通过使用SDS导出的API来调用分配器,以操作SDS指针(通常不需要,但有时程序可能想要执行更高级的操作)。这特别有用,当程序链接到SDS时使用与SDS不同的分配器。

访问SDS使用的分配器的API由三个函数组成:sds_malloc()sds_realloc()sds_free()

致谢和许可

SDS由Salvatore Sanfilippo创建,并按照BDS双条款许可发布。有关更多信息,请参阅此源分布中的LICENSE文件。

Oran Agra通过添加动态大小的头文件来改进SDS版本2,以节省小字符串的内存,并允许字符串大小超过4GB。

依赖项