当前位置: 首页 > 编程日记 > 正文

Simple Dynamic Strings(SDS)源码解析和使用说明二

在《Simple Dynamic Strings(SDS)源码解析和使用说明一》文中,我们分析了SDS库中数据的基本结构和创建、释放等方法。本文将介绍其一些其他方法及实现。(转载请指明出于breaksoftware的csdn博客)

字符串连接

SDS库提供下面两种方法进行字符串连接

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

sdscat函数在底层使用了sdscatlen去实现。sdscatlen方法第一个元素是需要被连接的SDS字符串,第二个参数是需要连接的内容起始地址,第三个是其内容的长度。和C语言中的连接函数不同,sdscatlen方法并不要求追加的内容要以NULL结尾,因为SDS字符串可以在内容中间承载NULL字符。但是sdscat则需要追加的字符串以NULL结尾,因为它没有提供长度参数。我们看下它们的实现:

sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;memcpy(s+curlen, t, len);sdssetlen(s, curlen+len);s[curlen+len] = '\0';return s;
}

sdscatlen方法中通过sdsMakeRoomFor方法获取需要被追加的sds对象,然后通过memcpy追加相关的内容到该对象的字符串末尾。最后修改SDS字符串中代表已经使用了空间长度的字段len,并把最后一位设置为NULL。我们需要关注下sdsMakeRoomFor方法的实现

sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;

sdsMakeRoomFor方法首先需要知道被追加的SDS字符串还有多少空余的空间,这步计算通过sdsavail方法实现,其实现也很简单,我们以SDS_TYPE_5和SDS_TYPE_8为例:

static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}

从这个设计可以看出,作者认为如果调用sdsavail方法时,这个SDS字符串可能是需要扩展空间了。如果此时它的类型是SDS_TYPE_5,则不经过任何计算,直接认为可用空间不够。如果不是空间最小的类型,则通过分配的了空间大小alloc减去已使用的空间大小len计算出还可用的空间大小。

再回到sdsMakeRoomFor方法中,如果判断发现SDS字符串剩余空间的大小足以承载追加的内容,则直接返回入参字符串对象。如果不够,则需要计算需要的长度

    len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;

SDS_MAX_PREALLOC是1M的空间,如果追加的长度和原始长度之和在1M以内,则新的空间是它们和的2倍大;如果大于1M,则在它们之和的基础上增加1M。这就是预分配的逻辑,这样设计可以预防频繁的的内存分配操作,当然相应的也会增加一定的内存浪费。但是总的来说,在目前CPU资源比内存资源贵的场景下,空间换时间还是比较好的。

然后通过新空间的大小匹配SDS字符串类型,如果新老类型相同,则直接使用realloc操作扩展空间。如果类型不同,则需要重新分配一个空间,将老空间里内容复制过来,最后还要将老空间释放掉。

    type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, newlen);return s;
}

sdsMakeRoomFor方法在之后的代码中我们会反复见到,但是见到它就要有个印象:通过它操作的SDS字符串可能是原始的,也可能是将原始空间释放后重新分配的。

我们再看下它们的使用样例:

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

SDS字符串库还提供了连接两个SDS字符串的方法

sds sdscatsds(sds s, const sds t);

其底层还是调用了sdscatlen方法

sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));
}

它的使用方法是:

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

还有一个特殊的方法,它是用于扩张SDS字符串的长度

sds sdsgrowzero(sds s, size_t len);

如果入参s的长度已经大于等于len了,则不作任何操作;否则增加s的长度到len,并使用NULL去填充多出来的空间。它的实现和sdscatlen很像,只是填充的长度是总长,而不是追加的长度;填充的字符是NULL而已。

sds sdsgrowzero(sds s, size_t len) {size_t curlen = sdslen(s);if (len <= curlen) return s;s = sdsMakeRoomFor(s,len-curlen);if (s == NULL) return NULL;/* Make sure added region doesn't contain garbage */memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */sdssetlen(s, len);return s;
}

我们看下使用例子:

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!

字符串格式化

字符串格式化是非常有用的工具,它可以让我们通过指定格式生成一个字符串。SDS字符串库也提供了相应的方法:

sds sdscatprintf(sds s, const char *fmt, ...) {va_list ap;char *t;va_start(ap, fmt);t = sdscatvprintf(s,fmt,ap);va_end(ap);return t;
}

这个方法底层调用了sdscatvprintf方法:

sds sdscatvprintf(sds s, const char *fmt, va_list ap) {va_list cpy;char staticbuf[1024], *buf = staticbuf, *t;size_t buflen = strlen(fmt)*2;/* We try to start using a static buffer for speed.* If not possible we revert to heap allocation. */if (buflen > sizeof(staticbuf)) {buf = s_malloc(buflen);if (buf == NULL) return NULL;} else {buflen = sizeof(staticbuf);}/* Try with buffers two times bigger every time we fail to* fit the string in the current buffer size. */while(1) {buf[buflen-2] = '\0';va_copy(cpy,ap);vsnprintf(buf, buflen, fmt, cpy);va_end(cpy);if (buf[buflen-2] != '\0') {if (buf != staticbuf) s_free(buf);buflen *= 2;buf = s_malloc(buflen);if (buf == NULL) return NULL;continue;}break;}/* Finally concat the obtained string to the SDS string and return it. */t = sdscat(s, buf);if (buf != staticbuf) s_free(buf);return t;
}

这和我们之前使用C语言的格式化不同,这个方法将格式化后的字符串追加到入参第一个参数s的后面。怎么感觉第一个参数s非常鸡肋,我们看看调用的例子就感觉到了:

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

我们再看一个使用特例:

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

这个方法将一个整形转换成一个字符串。但是这个方法在此次转换场景中还是非常低效的,我们会在之后介绍专门针对整形数字转换成字符串的高效方法。

整形数组转字符串

sds提供下面这种方法将整形数字数字转换成字符串,当然这个转换的效率要比sdscatprintf要高:

sds sdsfromlonglong(long long value) {char buf[SDS_LLSTR_SIZE];int len = sdsll2str(buf,value);return sdsnewlen(buf,len);
}

我们看下sdsll2str为什么比较高效:

int sdsll2str(char *s, long long value) {char *p, aux;unsigned long long v;size_t l;/* Generate the string representation, this method produces* an reversed string. */v = (value < 0) ? -value : value;p = s;do {*p++ = '0'+(v%10);v /= 10;} while(v);if (value < 0) *p++ = '-';/* Compute length and add null term. */l = p-s;*p = '\0';/* Reverse the string. */p--;while(s < p) {aux = *s;*s = *p;*p = aux;s++;p--;}return l;
}

这个方法底层没有使用字符串格式化这种比较通用但是效率不高的方法,它将入参整形数字从后向前逐个分解出来转换成字符,然后再逆序将这些字符写回到字符串内存空间中。这个函数还可以处理负数,相应的SDS字符串库还提供了针对无符号整形数的转换函数sdsull2str,其实现和sdsll2str非常类似,这儿我就不详细说明了。

字符串裁剪和截取

字符串裁剪操作在实际工作中也是常用的。SDS字符串库通过下面这个方法实现裁剪

sds sdstrim(sds s, const char *cset) {char *start, *end, *sp, *ep;size_t len;sp = start = s;ep = end = s+sdslen(s)-1;while(sp <= end && strchr(cset, *sp)) sp++;while(ep > sp && strchr(cset, *ep)) ep--;len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);s[len] = '\0';sdssetlen(s,len);return s;
}

该函数第二个参数是一个C语言的字符串,其以NULL结尾,包含了需要被裁剪掉的字符。这个裁剪操作通过两个while循环,分别从待裁剪的字符串最前和最后两个位置向另一个方向进行检索,只要遇到需要被裁剪的就继续探索下一个字符,如果是不需要裁剪的就终止当前方向的探测和检索。最终确定剩下的字符串的起始地址后,将这段空间内容复制到SDS字符串内容起始处,并设置结尾符NULL和已使用的空间长度记录变量len。我们来看个使用例子:

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

可见my和string之间的空格没有被裁剪,虽然它在要被裁剪的字符串列表中。这儿还有个地方需要说明下,该字符串裁剪操作没有进行字符串空间的再分配,而是利用原来的字符串空间进行处理的。

字符串截取操作是通过指定字符串的前后下标方式,截取其区间的内容并返回。这块操作通过下面方法实现的:

void sdsrange(sds s, int start, int end) {size_t newlen, len = sdslen(s);if (len == 0) return;if (start < 0) {start = len+start;if (start < 0) start = 0;}if (end < 0) {end = len+end;if (end < 0) end = 0;}newlen = (start > end) ? 0 : (end-start)+1;if (newlen != 0) {if (start >= (signed)len) {newlen = 0;} else if (end >= (signed)len) {end = len-1;newlen = (start > end) ? 0 : (end-start)+1;}} else {start = 0;}if (start && newlen) memmove(s, s+start, newlen);s[newlen] = 0;sdssetlen(s,newlen);
}

可见其实现就是简单的下标计算,然后内容复制,最后是结尾符NULL设置和已使用空间长度len字段设置。从代码中我们可以看出,用户传入的下标参数可以是正数,也可以是负数。正数代表下标从起始位置开始,负数代表下标从结束位置开始。这个操作也没有进行内存的重新分配。其使用样例见下:

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

字符串复制

字符串复制的操作也非常简单,SDS字符串库提供了两个方法,一个是供C语言字符串使用的

sds sdscpy(sds s, const char *t) {return sdscpylen(s, t, strlen(t));
}

另一个则是可以复制包含NULL字符的二进制数据的

sds sdscpylen(sds s, const char *t, size_t len) {if (sdsalloc(s) < len) {s = sdsMakeRoomFor(s,len-sdslen(s));if (s == NULL) return NULL;}memcpy(s, t, len);s[len] = '\0';sdssetlen(s, len);return s;
}

字符串转可视化

因为SDS字符串中可以包含二进制字符,所以当我们试图打印出这个字符串时,printf方法可能输出不可见的字符。这在调试一一段数据的时候可能比较有用,SDS提供了下面这个方法将字符串中不可见字符和转义字符都转换成可见的内容:

sds sdscatrepr(sds s, const char *p, size_t len) {s = sdscatlen(s,"\"",1);while(len--) {switch(*p) {case '\\':case '"':s = sdscatprintf(s,"\\%c",*p);break;case '\n': s = sdscatlen(s,"\\n",2); break;case '\r': s = sdscatlen(s,"\\r",2); break;case '\t': s = sdscatlen(s,"\\t",2); break;case '\a': s = sdscatlen(s,"\\a",2); break;case '\b': s = sdscatlen(s,"\\b",2); break;default:if (isprint(*p))s = sdscatprintf(s,"%c",*p);elses = sdscatprintf(s,"\\x%02x",(unsigned char)*p);break;}p++;}return sdscatlen(s,"\"",1);
}

这个方法的实现原理也很简单。对于可见字符,则直接显示。对于被反斜杠转义的字符,增加一个反斜杠使得反斜杠自身被转义,从而显示出可打印的内容。对于剩下的不可见的,则将其转成8进制数字输出。我们看下例子:

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"

字符串拆分

字符串拆分是指将一个一定规则的字符串,按照某种分隔符进行切分,从而得到一组切分后的字符串的操作。举个例子,我们要对下面这串字符按照|-|为切割符进行切割

foo|-|bar|-|zap

最终得到的结果是一组字符串,它们分别为foo、bar和zap。如果以-为切割符,则切割后的字符串数组包含:foo|、|bar|、|zap。我们看看SDS字符串库通过什么接口完成这个功能的

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

第一个参数是需要被切割的字符串的指针,第二个参数是该字符串的长度。第三个参数是分隔符字符串的起始地址,第四个则是分隔符字符串的长度。通过这种形式传递进去的字符串,在其内容中是可以包含NULL的,因为提供了长度信息就意味着不用以NULL来查找字符串结尾了。该函数的返回值是一个SDS字符串数组的起始地址,这个数组的长度通过第五个参数返回。我们看下其实现:

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {int elements = 0, slots = 5, start = 0, j;sds *tokens;if (seplen < 1 || len < 0) return NULL;tokens = s_malloc(sizeof(sds)*slots);if (tokens == NULL) return NULL;if (len == 0) {*count = 0;return tokens;}

首先预分配了5个槽位用于存储切割留下的数据,然后遍历整个字符串空间,并使用分隔符进行对比,取出分割后的字符串

    for (j = 0; j < (len-(seplen-1)); j++) {/* make sure there is room for the next element and the final one */if (slots < elements+2) {sds *newtokens;slots *= 2;newtokens = s_realloc(tokens,sizeof(sds)*slots);if (newtokens == NULL) goto cleanup;tokens = newtokens;}/* search the separator */if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {tokens[elements] = sdsnewlen(s+start,j-start);if (tokens[elements] == NULL) goto cleanup;elements++;start = j+seplen;j = j+seplen-1; /* skip the separator */}}/* Add the final element. We are sure there is room in the tokens array. */tokens[elements] = sdsnewlen(s+start,len-start);

如果预分配的5个槽位不够,则在填充即将满了的时候,让槽位数量增加一倍。这些操作如果都成功,则返回SDS字符串数组,否则清空整个申请的空间

    if (tokens[elements] == NULL) goto cleanup;elements++;*count = elements;return tokens;cleanup:{int i;for (i = 0; i < elements; i++) sdsfree(tokens[i]);s_free(tokens);*count = 0;return NULL;}
}

在我们调用该方法获取到切割后的字符串数组后,我们要释放该数组的所有空间,以防止内存溢出问题。释放的方法是:

void sdsfreesplitres(sds *tokens, int count) {if (!tokens) return;while(count--)sdsfree(tokens[count]);s_free(tokens);
}

最后看下这些方法的使用样例:

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!

命令行字符串拆解

之前介绍的字符串拆解方法要求入参字符串是严格按照一定方式布局的。然而用户输入形式的字符串,比如命令行,则非常可能不严格遵守格式。比如命令行中我们一般以空格分隔调用程序和其参数,但是并不严格要求使用几个空格去分隔

call "Sabrina"    and "Mark Smith\n"

上面这个命令行式的字符串则不能使用之前介绍的通过分隔符切割出各个字符串的方法,这时要准确切割需要使用:

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

这个方法第一个参数是待切割的字符串首地址,当然是以NULL结尾的。返回值是切割后的字符串数组首地址,第二个参数是用于传出这个数组的长度。它的实现就是从头向尾遍历整个字符串空间,然后切分出各个字符串。由于过程比较简单,但是代码比较长,我就不在这儿贴出来了。唯一要说的,可能引号匹配的场景截取稍微复杂点,因为引号里的空格是不能当成分隔符的。我们看下通过上面函数切分本节例子的结果

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

字符串数组转字符串

有些场景下,我们会需要将“字符串切割成字符串数组”的行为逆过来,让字符串数组中对象逐个连接变成一个字符串。SDS字符串库提供了如下两个方法:

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

sdsjoin方法针对C语言的字符串数组。第一个参数是字符串数组首地址。第二个参数是该数组的长度。第三个参数是连接各个字符串元素的“分隔符”字符串首地址。第四个参数是“分隔符”字符串的长度。sdsjoinsds方法则是针对sds字符串数组的。从这种参数设计可以看出,分隔符是可以包含NULL的,因为它提供了其长度信息。它们的实现也很简单:

sds sdsjoin(char **argv, int argc, char *sep) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscat(join, argv[j]);if (j != argc-1) join = sdscat(join,sep);}return join;
}sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscatsds(join, argv[j]);if (j != argc-1) join = sdscatlen(join,sep,seplen);}return join;
}

我们再看下其使用样例:

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

缩减字符串空间

SDS字符串在初次创建时,其分配空间大小就是使用了的空间大小。但是由于字符串连接等操作,会触发sdsMakeRoomFor方法,从而产生预分配的现象。这个时候往往被使用的空间大小只占已分配空间的一半。在大部分场景下,这种设计没有什么问题。但是对于内存特别紧张的时候,可能需要缩减这些字符串空间。SDS提供了如下方法实现空间缩减:

sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);type = sdsReqType(len);hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {newsh = s_malloc(hdrlen+len+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, len);return s;
}

这个操作一定会产生内存重分配的问题,所以它还是比较消耗效率的。好在需要它的场景不太多。

C语言字符串格式化SDS字符串

C语言中的字符串是以NULL结尾的,而SDS字符串可以包含NULL。如果我们希望SDS字符串按照C语言字符串格式一样,以NULL结尾,则可以调用如下方法:

void sdsupdatelen(sds s) {int reallen = strlen(s);sdssetlen(s, reallen);
}

这步操作非常简单,它只是以C语言字符串方式重新计算长度,并设置长度信息。它并没有进行字符串空间的重分配。我们看下例子:

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

基于上面的介绍,我们可以得知SDS字符串存在如下的特点:

  • 便于使用。我们可以像使用C语言中字符串一样接受和使用SDS字符串。比如我们可以
sds mystring = sdsnew("Hello World!");
mystring[0] = 'h';
printf("%s\n", mystring);
……
output>hello World!
  • 二进制安全。SDS字符串在头部使用len字段表示了buf中被使用了的空间长度,也就是说buf空间内容可以不用以NULL结尾。这种设计可以让SDS字符串承载包括NULL在内的一些二进制数据。
  • 执行高效。在字符串连接过程中,如果每次连接都要重新分配内存以承载更多的数据,会导致效率下降。而我们在SDS字符串头结构中看到有用于保存已分配空间的长度和已使用的空间的长度。

当然它也有相应的缺陷:

  • 可能要经常分配空间。一般一个字符串在第一次执行连接操作时,会发生原来字符串空间被释放,新空间被申请的过程。虽然作者做了优化,但是这个操作还是在所难免的。
  • 非线程安全的。我们在代码中没有看到任何线程安全性的辅助操作,所以它是线程非安全的。

本文的很多知识和样例来源于GitHub上的SDS说明:https://github.com/antirez/sds。我本来是想翻译这篇文章的,但是翻译过后感觉如果是内容直译可能不太符合大众的阅读口味,所以我就穿插的代码将它重新写了一遍。

相关文章:

亚马逊机器学习服务:深入研究AWS SageMaker

作者 | Manish Manalath译者 | Shawn编辑 | Carol出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 机器学习是一个从数据中发现模式的强大概念。但是&#xff0c;如果您尝试过从零开始构建机器模型&#xff0c;那么您一定知道设计一个可扩展的机器学习工作流是多大的…

Java Timer 定时器的使用

一、延时执行首先&#xff0c;我们定义一个类&#xff0c;给它取个名字叫TimeTask&#xff0c;我们的定时任务&#xff0c;就在这个类的main函数里执行。 代码如下&#xff1a;package test;import java.util.Timer;public class TimeTaskTest { public static void main(Str…

Redis源码解析——前言

今天开启Redis源码的阅读之旅。对于一些没有接触过开源代码分析的同学来说&#xff0c;可能这是一件很麻烦的事。但是我总觉得做一件事&#xff0c;不管有多大多难&#xff0c;我们首先要在战略上蔑视它&#xff0c;但是要在战术上重视它。除了一些高大上的技术&#xff0c;我们…

asp.net客户端脚本验证小技巧

通用的客户端脚本验证 Code//验证客户端function checkclient() { var list document.all; for(var i0 ;i<list.length; i) { var h list[i].hint; if(h ! null && h ! "") { if(list[i].isdrop"…

5个可以帮助你提高工作效率的新AI工具

作者 | Kyrylo Lyzanets译者 | 火火酱编辑 | Carol出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 毫无意义的新闻、故事和活动会占用你每天多少的工作时间&#xff1f;假如你是一名需要高绩效的高管或专业人士&#xff0c;如果在工作中可以不分心&#xff0c;那你…

Centos6.5更换163源 epel源

想必大家都遇到过&#xff0c;安装新的centos系统&#xff0c;使用yum去安装软件的时候&#xff0c;要么找不到&#xff0c;要么慢的让人发疯。网上其实办法很多&#xff0c;直接更换163源就ok&#xff0c;但是基本所有的文章都是直接wget下163的源&#xff0c;但是不知道为什么…

图模型+Bert香不香?完全基于注意力机制的图表征学习模型Graph-Bert

作者 | Jiawei Zhang、Haopeng Zhang、Congying Xia、Li Sun译者 | 凯隐编辑 | Jane出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;【导读】本文提出了一种全新的图神经网络 Graph-Bert&#xff0c;仅仅基于 Attention 机制而不依赖任何类卷积或聚合操作…

闭关纪要17.Google app engine的简单应用

在上面用了十一篇博客的文章详细的介绍了&#xff0c;Step1账户登录系统之后&#xff0c;从现在开始&#xff0c;继续写闭关纪要&#xff0c;因为Step1账户登录系统也是闭关工作的一部分&#xff0c;因此保留序号&#xff0c;这篇纪要在上次的闭关纪要5.WML,UTF-8,BOM,签名及其…

Redis源码解析——内存管理

在《Redis源码解析——源码工程结构》一文中&#xff0c;我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库。在linux系统中&#xff0c;Redis默认使用jemalloc库。当然用户可以指定使用tcmalloc或者libc的原生内存管理库。本文介绍的内容是在这些库的基础上&#…

poj_2479 动态规划

题目大意 给定一列数&#xff0c;从中选择两个不相交的连续子段&#xff0c;求这两个连续子段和的最大值。 题目分析 典型的M子段和的问题&#xff0c;使用动态规划的方法来解决。 f[i][j] 表示将A[1...i] 划分为j个不相交连续子串&#xff0c;且A[j]属于第i个子串&#xff0c;…

Redis源码解析——字典结构

C语言中有标准的字典库&#xff0c;我们可以通过pair(key,value)的形式存储数据。但是C语言中没有这种的库&#xff0c;于是就需要自己实现。本文讲解的就是Redis源码中的字典库的实现方法。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 一般情况下&#xf…

十步,教你把Python运行速度提升 30%

作者 | Martin Heinz译者 | 陆离编辑 | Jane出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;【导读】一直以来&#xff0c;诟病 Python语言的人经常说&#xff0c;他们不想使用的一个原因是 Python 的速度太慢了。不管使用哪一种编程语言&#xff0c;程序…

字符串转换成NSDate类型的 为nil解决方法

方法一 通过下列函数来解决 但是得到的日期会改变 修改方法fix- (NSDate *)timeForString:(NSString *)string {NSMutableString *timeString [[NSMutableString alloc] initWithString:string]; [timeString setString:[timeString stringByReplacingOccurrence…

Redis源码解析——字典基本操作

有了《Redis源码解析——字典结构》的基础&#xff0c;我们便可以对dict的实现进行展开分析。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 创建字典 一般字典创建时&#xff0c;都是没有数据的&#xff0c;但是字典类型需要确定&#xff0c;所以我们看到R…

[转]控制 Cookie 的作用范围

默认时&#xff0c;网站的所有 Cookies 都一起被存储在客户端&#xff0c;并且所有 Cookies 连同网站的任何请求一起被发送到服务器。换句话说&#xff0c;网站中的每个页面都能够为网站获取所有的 Cookies。但是&#xff0c;你能够通过两个方式来设置 Cookies 的作用范围&…

强化学习70年演进:从精确动态规划到基于模型

作者 | Nathan Lambert译者 | 泓礼编辑 | 夕颜出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09;【导读】这是一份帮你了解强化学习算法本质的资源&#xff0c;无需浏览大量文档&#xff0c;没有一条公式&#xff0c;非常适合学生和研究人员阅读。作为强化学习研究人员…

Android ActionBar相关

1.Android 5.0 删除ActionBar下面的阴影 于Android 5.0假设你发现的ActionBar下面出现了阴影&#xff0c;例如&#xff0c;下面的设置&#xff0c;以消除阴影&#xff1a; getActionBar().setElevation(0); Android 5.0之前能够用以下代码消除阴影&#xff1a; <item name&q…

Redis源码解析——字典遍历

之前两篇博文讲解了字典库的基础&#xff0c;本文将讲解其遍历操作。之所以将遍历操作独立成一文来讲&#xff0c;是因为其中的内容和之前的基本操作还是有区别的。特别是高级遍历一节介绍的内容&#xff0c;充满了精妙设计的算法智慧。&#xff08;转载请指明出于breaksoftwar…

开发者在行动!中国防疫开源项目登上GitHub TOP榜

整理 | 唐小引出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;【导读】用开发者们的方式支援这场没有硝烟的战争&#xff01;截止北京时间 1 月 28 日下午 15:47&#xff0c;全国确诊新型冠状病毒的数字已经到达了 4586 例&#xff0c;疑似高达 6973 例&#xff0c…

像童话一样学习OSPF原理

可以把整个网络&#xff08;一个自治系统AS&#xff09;看成一个王国&#xff0c;这个王国可以分成几个 区(area)&#xff0c;现在我们来看看区域内的某一个人(你所在的机器root)是怎样得到一张 世界地图(routing table)的。   首先&#xff0c;你得跟你周围的人&#xff08;…

队列——PowerShell版

继续读啊哈磊《啊哈&#xff01;算法》感悟系列——队列 地铁售票处排队&#xff0c;先来的人先到队首先买完先走&#xff0c;后来的人排在队尾等候后买完后走。 想买票&#xff0c;必须排在队尾&#xff1b;买完票&#xff0c;只能从队首离开。 这种先进先出&#xff08;First…

Redis源码解析——双向链表

相对于之前介绍的字典和SDS字符串库&#xff0c;Redis的双向链表库则是非常标准的、教科书般简单的库。但是作为Redis源码的一部分&#xff0c;我决定还是要讲一讲的。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基本结构 首先我们看链表元素的结构。因为…

12月第三周安全要闻回顾:浏览器安全不容忽视,SSL弱点影响网站安全

本周&#xff08;081215至081221&#xff09;安全方面的新闻众多&#xff0c;主要集中在***与威胁趋势方面。浏览器安全方向波澜起伏&#xff0c;微软推出了针对上周公开的IE7新漏洞的紧急安全补丁&#xff0c;但目前互联网上针对该漏洞的大规模***仍在继续&#xff0c;******的…

GPT2文本生成有问题?这里有些潜在解决思路

作者 | Leo Gao译者 | 凯隐编辑 | 夕颜出品 | AI科技大本营&#xff08;ID: rgznai100&#xff09; 【导读】在过去的一年中&#xff0c;人们对文本生成模型的兴趣重新燃起&#xff0c;这在很大程度上要归功于GPT2&#xff0c;它主要展示了使用更大模型、更大数据和更大计算量的…

HTML5学习之二:HTML5中的表单2

&#xff08;本内容部分节选自《HTML 5从入门到精通》) 对表单的验证 ———————————————————————————————————————————————————————— •1、required属性 required属性主要目的是确保表单控件中的值已填写。在提交时&…

Redis源码解析——有序整数集

有序整数集是Redis源码中一个以大尾&#xff08;big endian&#xff09;形式存储&#xff0c;由小到大排列且无重复的整型集合。它存储的类型包括16位、32位和64位的整型数。在介绍这个库的实现前&#xff0c;我们还需要先熟悉下大小尾内存存储机制。&#xff08;转载请指明出于…

GitHub标星1.2w+,Chrome最天秀的插件都在这里

作者 | Rocky0429来源 | Python空间&#xff08;ID: Devtogether&#xff09;大家好&#xff0c;我是 Rocky0429&#xff0c;一个沉迷 Chrome 不能自拔的蒟蒻...作为一个在远古时代用过什么 IE、360、猎豹等浏览器的资深器哥&#xff0c;当我第一次了解 Chrome 的时候&#xff…

基础篇 第四节 项目进度计划编辑 之 任务关联性设定

1.任务关联性的类型 ◎完成 —— 开始 FS ◎开始 —— 开始 SS ◎开始 —— 完成 SF 完成 —— 完成 FF 2.设定任务关联性 三种方法&#xff1a; ◎在条形图中直接拖拽 ◎在“前置任务”列中编辑 ◎在“任务信息”中的“前置任务”选项卡中编辑 3.设定“延隔时间” 延隔时间小于…

开坑,写点Polymer 1.0 教程第3篇——组件注册与创建

之前一篇算是带大家大致领略了一下Polymer的风采。这篇我们稍微深入一丢丢&#xff0c;讲下组件的注册和创建。 创建自定义组件的几种方式 这里我们使用Polymer函数注册了一个自定义组件"my-element" // register an element Polymer({is: my-element,created: funct…

Redis源码解析——Zipmap

本文介绍的是Redis中Zipmap的原理和实现。&#xff08;转载请指明出于breaksoftware的csdn博客&#xff09; 基础结构 Zipmap是为了实现保存Pair(String,String)数据的结构&#xff0c;该结构包含一个头信息、一系列字符串对&#xff08;之后把一个“字符串对”称为一个“元素…