redis链表结构和简单动态字符串(SDS)

1.双向链表

redis中的普通链表是双向链表。通过链表节点结构体可知有全驱节点和后继节点。

1.链表节点和链表

//adlist.h
typedef struct listNode {struct listNode *prev;    //前驱节点struct listNode *next;    //后继节点void *value;                //节点值
} listNode;//链表结构
typedef struct list {listNode *head;    //头节点listNode *tail;    //尾结点void *(*dup)(void *ptr);    //节点值复制函数void (*free)(void *ptr);    //节点值释放函数int (*match)(void *ptr, void *key);       //节点值比对函数unsigned long len;    //当前节点的个数
} list;

链表中的函数指针主要是对节点值的操作,包括复制,析构,判断是否相等。

2.迭代器

此外,Redis还为链表提供迭代器的功能,主要是对链表节点的封装,另外通过链表节点的前驱节点和后继节点,可以轻松的完成向前移动和向后移动。

//adlist.h
typedef struct listIter {listNode *next;    //指向的实际节点int direction;    //表示迭代器方向,向前还是向后
} listIter;/* Directions for iterators */
#define AL_START_HEAD 0    //从表头向表尾迭代
#define AL_START_TAIL 1    //从表尾向表头迭代

3.宏定义函数

这些看宏名字就很容易知道其功能的。

#define listLength(l) ((l)->len)
#define listFirst(l) ((l)->head)
#define listLast(l) ((l)->tail)
#define listPrevNode(n) ((n)->prev)
#define listNextNode(n) ((n)->next)
#define listNodeValue(n) ((n)->value)#define listSetDupMethod(l,m) ((l)->dup = (m))
#define listSetFreeMethod(l,m) ((l)->free = (m))
#define listSetMatchMethod(l,m) ((l)->match = (m))#define listGetDupMethod(l) ((l)->dup)
#define listGetFreeMethod(l) ((l)->free)
#define listGetMatchMethod(l) ((l)->match)

4.链表的操作

list *listCreate(void); //创建链表
void listRelease(list *list);   //释放链表,会释放内存,就是完全释放链表了
void listEmpty(list *list);     //置空链表,没有释放内存的
list *listAddNodeHead(list *list, void *value); //添加节点在头部
list *listAddNodeTail(list *list, void *value); //在尾部添加节点
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);   //删除该节点listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
void listRewind(list *list, listIter *li);
void listRewindTail(list *list, listIter *li);
void listRotateTailToHead(list *list);
void listRotateHeadToTail(list *list);
void listJoin(list *l, list *o);//有关迭代器的操作
listIter *listGetIterator(list *list, int direction); //创建一个链表迭代器,并根据传入的direction来返回链表的头或尾节点
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);

链表的创建和添加节点这些比较好理解,就不多说。这里主要讲解下有关迭代器的操作。

4.1. listGetIterator

listIter *listGetIterator(list *list, int direction)
{listIter *iter;if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;// 根据迭代方向,设置迭代器的起始节点if (direction == AL_START_HEAD)    //这个表示迭代器的方向是从头到尾iter->next = list->head;    //迭代器对应的节点就是头节点elseiter->next = list->tail;iter->direction = direction;    //记录迭代方向return iter;
}

4.2.listRewind和listRewindTail

handleClientsWithPendingReadsUsingThreads函数中使用到的。

在列表专用迭代器结构中创建迭代器,listRewind即是通过传入一个listIter变量,从而创建了迭代器,从而可以得到链表中的节点了。

void listRewind(list *list, listIter *li) {li->next = list->head;li->direction = AL_START_HEAD;
}void listRewindTail(list *list, listIter *li) {li->next = list->tail;li->direction = AL_START_TAIL;
}

4.3.listNext

返回迭代器当前所指向的节点,即是iter->next。

这个函数也是在handleClientsWithPendingReadsUsingThreads中有使用到的,通过一个 while((ln = listNext(&li)))循环获取链表中的节点

listNode *listNext(listIter *iter)
{listNode *current = iter->next;if (current != NULL) {//之后要更行迭代器当前所指向的节点if (iter->direction == AL_START_HEAD)iter->next = current->next;    //保存下一个节点,防止当前节点被删除而造成指针丢失elseiter->next = current->prev;}return current;
}

4.4.listDup,复制链表

前面说了那么多操作,其实就是为了把它们组合起来使用,handleClientsWithPendingReadsUsingThreads函数就是把他们组合起来使用的。也比如listDup函数,就是上面的综合体。

list *listDup(list *orig)
{list *copy;listIter iter;listNode *node;//创建链表if ((copy = listCreate()) == NULL)return NULL;// 设置节点值处理函数copy->dup = orig->dup;copy->free = orig->free;copy->match = orig->match;listRewind(orig, &iter);    //迭代整个输入链表while((node = listNext(&iter)) != NULL) {void *value;if (copy->dup) {    // 复制节点值到新节点value = copy->dup(node->value);if (value == NULL) {listRelease(copy);return NULL;}} elsevalue = node->value;if (listAddNodeTail(copy, value) == NULL) {    //将节点添加到链表listRelease(copy);return NULL;}}return copy;
}

2.简单动态字符串

2.1为什么不用char传统数组来表示字符串

Redis使用一种称为 简单动态字符串(Simple Dynamic Strings,SDS) 来存储字符串和整型数据。 这是为什么呢?

首先想想c语言字符串的一些问题

  • 获取字符串长度strlen,效率低。其时间复杂度是O(N)。
  • 缓冲区溢出问题。使用mepcy(a,b,n)时候,可能a长度不够,就导致溢出。
  • 二进制不安全。在 C 语言中,字符串实际上是使用字符 '\0'终止的一维字符数组。所以在 C 语言获取一个字符串长度,会逐个字符遍历,直到遇到代表字符串结尾的空字符为止。这就会导致二进制安全问题。通俗的讲,C 语言中,用 “\0” 表示字符串的结束,如果字符串本身就有 “\0” 字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。
  • 修改字符串可能需要进行多次分配内存,耗时耗空间。C 字符串本身不记录字符串的长度,所以对于一个包含了 N 个字符的 C 字符串来说, 这个 C 字符串的底层实现总是一个 N+1 个字符长的数组。因为 C 字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或缩短一个 C 字符, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作。比如执行增长操作之前需要先进行内存重分配,以扩展底层数组的空间大小,如果忘了就会导致缓冲区溢出。然而内存重分配需要执行系统调用,所以它通常是一个比较耗时的操作。

所以 Redis 为 SDS 设计了冗余空间,追加时只要内容不是太大,是可以不必重新分配内存的。

基于以上的问题,Redis就设计了SDS。

2.2 SDS类型

Redis为了灵活的保存不同大小的字符串节省内存空间,设计了不同的结构头sdshdr64sdshdr32sdshdr16sdshdr8和sdshdr5(已不再使用)。

2.2.1SDS结构

//sds.h
typedef char *sds;struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
//还有sdshdr32,sdshdr64没有展示.......................//这些就是对应结构体中的flags
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7    //二进制是0b0111,用来按位与
#define SDS_TYPE_BITS 3

虽然结构头不同,但是他们都具有相同的属性:

  • len:字符数组buf实际使用的大小,也就是字符串的长度(不包括'\0')
  • alloc:字符数组buf分配的空间大小,不包含结束标识符('\0')
  • flags:标记SDS的类型:sdshdr64、sdshdr32、sdshdr16、sdshdr8
  • buf[]:字符数组,用来存储实际的字符数据

有成员len就可以常数时间复杂度得到sds字符串的长度了,也因为以len 属性而不是空字符‘\0’来判断字符串是否结束,那也就可以存放二进制数据了。

redis一些设计点 

Redis使用了_attribute_ (( __packed__))节省内存空间,它可以告诉编译器使用紧凑的方式分配内存,不使用字节对齐的方式给变量分配内存。这也是代表着sdshdr结构体是按1字节对齐(默认结构体会按其所有变量大小的最小公倍数做字节对齐),即结构体的各属性是连续存储的,可以使用flags = s[-1] 这种操作。

在结构体定义中,可以看到最后一个buf数组是没有设置大小的,这种放在结构体中最后一个元素位置并且没有设置大小的数组称为柔性数组,它可以在程序运行过程中进行动态内存分配。

2.3宏定义SDS_HDR_VAR和SDS_HDR

//sds.h
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

关于 sdshdr##T ,不太懂,在这里理解成它将 sdshdrT 连接在一起,即表示不同的 sdshdr 类型。

关于 SDS_HDR_VAR(T,s), 参数 s 是sdshdr 结构体中的字符串指针,即等价于 buf,参数 T 则是表示不同类型的 sdshdr,取值可以为 8/16/32/64。然后看其实现,struct sdshdr##T *sh 是宏定义的一个变量, void* 是将结果转换为void* 类型,以便 sh 接收。而 (s) - ( sizeof(struct sdshdr##T) )则表示指向结构体变量的地址。首先sizeof(struct sdshdr##T) 计算的大小不包含 buf (柔性数组特点),而 s 的指向的地址就跟在 sdshdr 之后。

通过sdslensdsavail两个函数来看看他们的区别和使用:

SDS_HDR是SDS_HDR(8,s)->len;

SDS_HDR_VAR是直接就使用SDS_HDR_VAR(8,s);sh->alloc->sh->len

所以 SDS_HDR(T, S) 就是返回一个 保存字符串 s 的结构体地址,然后就可以调用其成员。

SDS_HDR_VAR定义一个指向保存字符串 s 的结构体指针,即是先定义一个结构体指针sh然后再使用sh.

注意两者的差别:SDS_HDR_VAR定义变量保存结构体地址,SDS_HDR返回结构体地址。

函数中的s[-1],即表示 sdshdr 结构中的成员 flags,因为其结构体的各属性是连续存储的。通过 flags 我们可以判断 sdshdr 的类型。

//得到字符串长度,即是已使用长度
static inline size_t sdslen(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8:return SDS_HDR(8,s)->len;case SDS_TYPE_16:return SDS_HDR(16,s)->len;//...............................}return 0;
}//得到还没使用的长度
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);return sh->alloc - sh->len;}//......................}return 0;
}

还有一些其他关于sds属性的函数

这些函数的实现是相似的。

sdssetlen:修改 sds 的有效长度。

sdsinclen:增加 sds 的有效长度

sdsalloc:返回 sds 的已分配空间大小。也可以通过 sdsavail() + sdslen() 来获取。

sdssetalloc:设置 sds 的已分配空间大小。

//sds.h
//修改 sds 的有效长度
static inline void sdssetlen(sds s, size_t newlen) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8:SDS_HDR(8,s)->len = newlen;break;case SDS_TYPE_16:SDS_HDR(16,s)->len = newlen;break;//...............}
}//增加 sds 的有效长度
static inline void sdsinclen(sds s, size_t inc) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8:SDS_HDR(8,s)->len += inc;break;case SDS_TYPE_16:SDS_HDR(16,s)->len += inc;break;//...............}
}//返回 sds 的已分配空间大小。也可以通过 sdsavail() + sdslen() 来获取
/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8:return SDS_HDR(8,s)->alloc;case SDS_TYPE_16:return SDS_HDR(16,s)->alloc;//...............}return 0;
}//设置 sds 的已分配空间大小
static inline void sdssetalloc(sds s, size_t newlen) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_8:SDS_HDR(8,s)->alloc = newlen;break;case SDS_TYPE_16:SDS_HDR(16,s)->alloc = newlen;break;//.......................}
}

2.4.SDS的创建

要创建一个 sds 对象,首先要确认其 sds 结构属于哪一种,要根据字符串长度来选择 sdshdr 。

下面是根据字符串长度来确认 sds 类型的几个相关函数

//sds.c
//通过长度去获取类型
static inline char sdsReqType(size_t string_size) {if (string_size < 1<<8)return SDS_TYPE_8;if (string_size < 1<<16)return SDS_TYPE_16;//............
}//得到对应的结构体的大小
static inline int sdsHdrSize(char type) {switch(type&SDS_TYPE_MASK) {case SDS_TYPE_5:return sizeof(struct sdshdr5);case SDS_TYPE_8:return sizeof(struct sdshdr8);//............................}return 0;
}

创建函数sdsnewlen

创建sds的思路是:

  1. 根据字符串长度来计算出sds类型,
  2. 申请sdshdr的内存
  3. 根据sdsHdr的类型设置 len, alloc, flag 属性,
  4. 将字符串内容设置到sdsHdr中,并最后添加'\0'结尾
//sds.c
//用于标识, 创建sds时, 不初始化字符数组
extern const char *SDS_NOINIT;//如果init指针是NULL, 则将内存初始化为0, 如果init是SDS_NOINIT, 则不进行初始化
sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;//1.根据长度得出要使用的sds的类型char type = sdsReqType(initlen);    //如果类型是 5, 或者初始字符串长度是 0 , 则默认给 8if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;int hdrlen = sdsHdrSize(type);    //得到sds类型的大小unsigned char *fp; /* flags pointer. */assert(initlen + hdrlen + 1 > initlen);     //防止溢出//2.分配内存sh = s_malloc(hdrlen+initlen+1);    if (sh == NULL) return NULL;if (init==SDS_NOINIT)    //如果init是SDS_NOINIT, 则不进行初始化init = NULL;else if (!init)    //如果init指针是NULL, 则将内存初始化为0,memset(sh, 0, hdrlen+initlen+1);s = (char*)sh+hdrlen;    //字符串的开始位置fp = ((unsigned char*)s)-1;    //得到flags//3.根据sdsHdr的类型设置 len, alloc, flag 属性,switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);    //这是找到结构体初始的地址,并赋值给shsh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_16: {SDS_HDR_VAR(16,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_32: {SDS_HDR_VAR(32,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}case SDS_TYPE_64: {SDS_HDR_VAR(64,s);sh->len = initlen;sh->alloc = initlen;*fp = type;break;}}//4.将字符串内容设置到sdsHdr中if (initlen && init)memcpy(s, init, initlen);s[initlen] = '\0';    //最后添加'\0'结尾return s;
}

还有一些其他的创建函数,其就是内部调用了sdsnewlen。

//sds.c
//创建一个空的sds
sds sdsempty(void) {return sdsnewlen("",0);
}
//根据给定的C字符串创建sds
sds sdsnew(const char *init) {size_t initlen = (init == NULL) ? 0 : strlen(init);return sdsnewlen(init, initlen);
}
//复制一个sds
sds sdsdup(const sds s) {return sdsnewlen(s, sdslen(s));
}

2.5.预分配SDS内存,可以动态扩容

 sds是简单动态字符串,说明是可以自动扩容的。那sdsMakeRoomFor就是来实现这个功能的。

扩大sds字符串末尾的可用空间,以便调用方确保在调用此函数后,可以覆盖字符串末尾后最多addlen个字节,再加上一个nul项字节。

过程:

  1. 得到剩余的空间大小和sds类型,若剩余的空间>=要扩容的空间,那就直接返回即可。
  2. 若剩余的空间不够,则获取已使用的长度和该结构体起始的地址位置,并计算出新的长度(已使用的长度+要扩容的长度),并进行判断,最终得到新长度。
  3. 根据新长度来确定新的sds类型,并得到结构体大小,若旧类型和新类型是一致的,那就在原有内存上重分配,扩展空间;否则,重新分配空间,再设置好结构体的一下成员数据。
sds sdsMakeRoomFor(sds s, size_t addlen) {  //要扩容addlen长度void *sh, *newsh;//步骤1size_t avail = sdsavail(s);     //得到剩余的空间大小size_t len, newlen, reqlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;    //得到类型int hdrlen;    if (avail >= addlen) return s;//步骤2len = sdslen(s);    //得到已使用的长度sh = (char*)s-sdsHdrSize(oldtype);  //该结构体起始的地址位置reqlen = newlen = (len+addlen);     //已使用的长度+要扩容的长度,即是新的总长度assert(newlen > len);   /* Catch size_t overflow */if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;//步骤3type = sdsReqType(newlen);//根据新总长度来确定sds类型if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);      //得到结构体大小assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1); //重分配,扩展空间if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;        //字符串开始的位置} else {//表示 sds 类型发生了变化,分配新的内存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;
}

那么这个调用该函数不会改变函数返回的sds字符串的长度len,只会改变其可用的剩余空间。

那来看看这个函数的使用例子

sdscatlen是把字符串t追加到s的末尾。类似于c语言的char *strcat(char *dest, const char *src)。

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;
}

 sdscpylen函数就类似c语言的 void *memcpy(void *str1, const void *str2, size_t n)。

sds sdscpylen(sds s, const char *t, size_t len) {if (sdsalloc(s) < len) {    //总的分配的长度小于要复制的长度len,所以要预分配新空间s = sdsMakeRoomFor(s,len-sdslen(s));if (s == NULL) return NULL;}memcpy(s, t, len);s[len] = '\0';sdssetlen(s, len);return s;
}

每次需要字符串增长的时候,就会调用sdsMakeRoomFor,若是需要就进行预分配空间,就可以确保不会出现缓冲区溢出问题。

而通过空间预分配策略,Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。

2.6.字符串释放(惰性空间释放)

 SDS 提供了直接释放内存的方法——sdsfree,该方法通过对 s 的偏移,可定位到 SDS 结构体的首部,然后调用 s_free 释放内存。

void sdsfree(sds s) {if (s == NULL) return;s_free((char*)s-sdsHdrSize(s[-1]));
}

而 为了优化性能(减少申请内存的开销),SDS 也提供了不直接释放内存,而是通过重置统计值达到清空目的的方法——sdsclear。该方法仅将 SDS 的 len 归零,此处已存在的 buf 并没有真正被清除,新的数据可以覆盖写,而不用重新申请内存。

void sdsclear(sds s) {sdssetlen(s, 0);    //设置已使用的长度s[0] = '\0';
}

通过惰性空间释放策略,SDS 避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。

2.7. SDS总结

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/788743.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

27.ReentrantLock

1.与synchronized不同点&#xff1a; 可中断可以设置超时时间可以设置公平锁&#xff0c;公平锁就是为了解决饥饿线程&#xff0c;让线程排队&#xff0c;先进先出&#xff0c;先来的线程先执行。支持多个条件变量 2.与synchronized相同点都支持锁的可重入。 基本格式&#…

“崖山数据库杯”深圳大学程序设计竞赛(正式赛)M题 一图秒

“崖山数据库杯”深圳大学程序设计竞赛&#xff08;正式赛&#xff09;_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ (nowcoder.com) —————— 可以去牛客看题解&#xff1a; 题解 | #暂时没想法#_牛客博客 (nowcoder.net) —————— 上面的就是题解了。…

Web CSS笔记3

一、边框弧度 使用它你就可以制作盒子边框圆角 border-radius&#xff1a;1个值四个圆角值相同2个值 第一个值为左上角与右下角&#xff0c;第二个值为右上角与左下角3个值第一个值为左上角, 第二个值为右上角和左下角&#xff0c;第三个值为右下角4个值 左上角&#xff0c;右…

springboot之MybatisPlus

文章目录 一、ORM二、mybatis实际操作三、mybatis-plus 一、ORM 简单来说ORM就是一个能够帮我们把java中Bean类映射到数据库中。 使用mybatis-plus。 配置架包 <!-- MyBatisPlus依赖 --><dependency><groupId>com.baomidou</groupId><art…

垄断与商品化背景下的网络安全三大整合策略

我国的网络安全产业已经发展了20余年&#xff0c;大大小小的企业几乎覆盖了网络安全的所有领域。随着安全需求的逐渐递增&#xff0c;安全产品也朝着平台化、规模化发展&#xff0c;这就倒逼着安全厂商需要整合越来越多的安全能力&#xff0c;并与其产品相融合。这个过程&#…

【VSCode+Keil5+STM32CubeMX】开发环境配置

一、软件下载 二、软件安装 三、配置环境 四、验证开发环境 五、Keil与VS Code的同步 从0到1搭建VS Code Keil5 STM32CubeMX开发环境 优点 支持标准库HAL库LL库代码编辑更“现代化”&#xff1a;代码提示、函数跳转、更高自由度的定制主题等优点多端同步&#xff0c;VS Code和…

【LAMMPS学习】七、加速性能(3)通用技巧

7. 加速性能 7.1.基准测试 7.2.测试性能 7.3.通用技巧 以下是提高模拟性能的通用技巧。它们中的大多数只适用于当前性能中的某些模型和某些瓶颈&#xff0c;因此让您生成的计时数据作为指导。要预测这些选项会产生多大的差异&#xff0c;即使不是不可能&#xff0c;也是很难…

Jmeter02-1:参数化组件CVS

目录 1、Jmeter组件&#xff1a;参数化概述 1.1 是什么&#xff1f; 1.2 为什么&#xff1f; 1.3 怎么用&#xff1f; 2、Jmeter组件&#xff1a;参数化实现之CSV Data Set Config(重点中重点) 2.1 是什么&#xff1f; 2.2 为什么&#xff1f; 2.3 怎么用&#xff1f; …

Golang | Leetcode Golang题解之第5题最长回文子串

题目&#xff1a; 题解&#xff1a; func longestPalindrome(s string) string {if s "" {return ""}start, end : 0, 0for i : 0; i < len(s); i {left1, right1 : expandAroundCenter(s, i, i)left2, right2 : expandAroundCenter(s, i, i 1)if ri…

Mysql数据库getshell方法

今天摸鱼时候&#xff0c;突然有人问我不同的数据库getshell的方式&#xff0c;一时间我想到了mysql还有redis未授权访问到getshell的方式&#xff0c;但是仅仅第一时间只想到了这两种&#xff0c;我有查了查资料&#xff0c;找到了上面两种数据库getshell的补充&#xff0c;以…

【面试八股总结】传输控制协议TCP(三)

参考资料 &#xff1a;小林Coding、阿秀、代码随想录 一、TCP拥塞控制⭐ 1. 慢启动 – Slow Start 慢启动是指TCP连接刚建立&#xff0c;一点一点地提速&#xff0c;试探一下网络的承受能力&#xff0c;以免直接扰乱了网络通道的秩序。 慢启动算法&#xff1a; 初始拥塞窗口…

电商技术揭秘五:电商平台的个性化营销与数据分析

文章目录 引言1. 个性化营销的概念与价值1.1 个性化营销的定义1.1.1 个性化营销的基本概念1.1.2 个性化营销在电商领域的重要性 1.2 个性化营销的核心价值1.2.1 提升用户体验1.2.2 增加转化率和客户忠诚度1.2.3 优化营销资源配置 2. 用户画像与行为分析2.1 用户画像的构建2.1.1…

SpringBoot+thymeleaf完成视频记忆播放功能

一、背景 1)客户要做一个视频播放功能,要求是系统能够记录观看人员在看视频时能够记录看到了哪个位置,在下次观看视频的时候能够从该位置进行播放。 2)同时,也要能够记录是谁看了视频,看了百分之多少。 说明:由于时间关系和篇幅原因,我们这里只先讨论第一个要求,第…

智能小车测速(3.26)

模块介绍&#xff1a; 接线&#xff1a; VCC -- 3.3V 不能接5V&#xff0c;否则遮挡一次会触发3次中断 OUT -- PB14 测速原理&#xff1a; cubeMX设置&#xff1a; PB14设置为gpio中断 打开定时器2&#xff0c;时钟来源设置为内部时钟&#xff0c;设置溢出时间1s&#xff0c…

视频监控/云存储/AI智能分析平台EasyCVR集成时调用接口报跨域错误的原因

EasyCVR视频融合平台基于云边端架构&#xff0c;可支持海量视频汇聚管理&#xff0c;能提供视频监控直播、云端录像、云存储、录像检索与回看、智能告警、平台级联、智能分析等视频服务。平台兼容性强&#xff0c;支持多协议、多类型设备接入&#xff0c;包括&#xff1a;国标G…

【已解决】Error: error:0308010C:digital envelope routines::unsupported

前言 场景&#x1f3ac; 使用 Ant Design &#xff0c; 执行 npm run dev 出现异常。 文章目录 前言场景&#x1f3ac; 异常信息解决方案方案一(推荐)MAC | Linux 电脑成功⬇️ Windows 电脑 方案2&#xff1a; 不懂留言 JavaPub 异常信息 我直接异常信息&#xff0c;你可以…

Tomcat部署flowable出现consider increasing the maximum size of the cache

使用Apache Tomcat/8.5.32部署运行flowable-6.5.0时发现控制台有警告 问题原因&#xff1a;解决方法: 使用Apache Tomcat/8.5.32部署运行flowable-6.5.0时发现控制台有警告 01-Apr-2024 20:55:08.877 警告 [localhost-startStop-1] org.apache.catalina.webresources.Cache.ge…

拥塞控制算法系列之:Swift-谷歌2020年SIGCOM-包级别端到端TIMELY拥塞控制算法

核心要点&#xff1a; 谷歌 2020 SIGCOM基于delay的AIMD拥塞拆分EC和FC&#xff0c;时延敏感场景优势分别计算EC和FC的wnd&#xff08;最核心&#xff09;保障吞吐和低延迟。Swift 因利用延迟的简单性和有效性而闻名包级别的论文&#xff1a;https://dl.acm.org/doi/pdf/10.11…

C刊级 | Matlab实现GWO-BiTCN-BiGRU-Attention灰狼算法优化双向时间卷积双向门控循环单元融合注意力机制多变量回归预测

C刊级 | Matlab实现GWO-BiTCN-BiGRU-Attention灰狼算法优化双向时间卷积双向门控循环单元融合注意力机制多变量回归预测 目录 C刊级 | Matlab实现GWO-BiTCN-BiGRU-Attention灰狼算法优化双向时间卷积双向门控循环单元融合注意力机制多变量回归预测效果一览基本介绍程序设计参考…

stream使用

stream流式计算 在Java1.8之前还没有stream流式算法的时候&#xff0c;我们要是在一个放有多个User对象的list集合中&#xff0c;将每个User对象的主键ID取出&#xff0c;组合成一个新的集合&#xff0c;首先想到的肯定是遍历&#xff0c;如下&#xff1a; List<Long> u…