LuaJIT源码分析(三)字符串

LuaJIT源码分析(三)字符串

要表示一个字符串,核心就是需要知道字符串的长度,以及存放字符串具体数据的地址。lua的字符串是内化不可变的,也就是lua字符串变量存放的不是字符串的拷贝,而是字符串的引用。那么,每当新建一个字符串时,lua都会去检查当前虚拟机中是否已经存在相同的数据,如果有就可以直接拿来用,没有再进行创建。

为了实现这一机制,lua必然有一个全局的地方存放当前用到的所有字符串。luajit内部使用了散列桶来管理字符串,在已知字符串的hash值时,只需要一次整数比较就可以快速查找。基于此,相同的字符串在虚拟机中只存在一个副本。

luajit中用于表示字符串的数据结构如下:

// lj_obj.h
/* String object header. String payload follows. */
typedef struct GCstr {
  GCHeader;
  uint8_t reserved;	/* Used by lexer for fast lookup of reserved words. */
  uint8_t hashalg;	/* Hash algorithm. */
  StrID sid;		/* Interned string ID. */
  StrHash hash;		/* Hash of string. */
  MSize len;		/* Size of string. */
} GCstr;

GCHeader就是上一节所说的用于gc管理的通用数据结构;reserved用来表示是否为lua的保留关键字,它是一个uint8_t类型,对应保留字列表的下标;hashalg表示该字符串是否被二次hash;sid保存了该字符串的全局ID,而hash则是该字符串的hash值。最后len字段表示整个字符串的长度。然后,实际字符串的内容保存在len字段之后。假如有一个字符串"hello",那么它在luajit内部所填充的GCstr数据结构可能长这样:

LuaJIT源码分析(三)字符串1

系统中的所有字符串都保存在luajit中global_state的str字段,这个字段是StrInternState类型:

// lj_obj.h
/* String interning state. */
typedef struct StrInternState {
  GCRef *tab;		/* String hash table anchors. */
  MSize mask;		/* String hash mask (size of hash table - 1). */
  MSize num;		/* Number of strings in hash table. */
  StrID id;		/* Next string ID. */
  uint8_t idreseed;	/* String ID reseed counter. */
  uint8_t second;	/* String interning table uses secondary hashing. */
  uint8_t unused1;
  uint8_t unused2;
  LJ_ALIGN(8) uint64_t seed;	/* Random string seed. */
} StrInternState;

tab字段保存了所有字符串,它是个链表数组,每个新建的字符串先根据hash算法计算出自身的hash值,再根据当前StrInternState的mask,得到数组的索引,如果已经有元素,就插入到链表的头部。

LuaJIT源码分析(三)字符串2

下面我们来看下创建字符串的具体过程吧。入口是lj_str_new这个函数,它接受原始字符串指针和字符串长度作为参数:

// lj_str.c
/* Intern a string and return string object. */
GCstr *lj_str_new(lua_State *L, const char *str, size_t lenx)
{
  global_State *g = G(L);
  if (lenx-1 < LJ_MAX_STR-1) {
    MSize len = (MSize)lenx;
    StrHash hash = hash_sparse(g->str.seed, str, len);
    MSize coll = 0;
    int hashalg = 0;
    /* Check if the string has already been interned. */
    GCobj *o = gcref(g->str.tab[hash & g->str.mask]);
#if LUAJIT_SECURITY_STRHASH
    if (LJ_UNLIKELY((uintptr_t)o & 1)) {  /* Secondary hash for this chain? */
      hashalg = 1;
      hash = hash_dense(g->str.seed, hash, str, len);
      o = (GCobj *)(gcrefu(g->str.tab[hash & g->str.mask]) & ~(uintptr_t)1);
    }
#endif
    while (o != NULL) {
      GCstr *sx = gco2str(o);
      if (sx->hash == hash && sx->len == len) {
	if (memcmp(str, strdata(sx), len) == 0) {
	  if (isdead(g, o)) flipwhite(o);  /* Resurrect if dead. */
	  return sx;  /* Return existing string. */
	}
	coll++;
      }
      coll++;
      o = gcnext(o);
    }
#if LUAJIT_SECURITY_STRHASH
    /* Rehash chain if there are too many collisions. */
    if (LJ_UNLIKELY(coll > LJ_STR_MAXCOLL) && !hashalg) {
      return lj_str_rehash_chain(L, hash, str, len);
    }
#endif
    /* Otherwise allocate a new string. */
    return lj_str_alloc(L, str, len, hash, hashalg);
  } else {
    if (lenx)
      lj_err_msg(L, LJ_ERR_STROV);
    return &g->strempty;
  }
}

首先,luajit会根据字符串的内容,使用hash_sparse这个函数计算它的hash值。hash_sparse接受3个参数,全局的seed,字符串的内容,以及字符串的长度。使用seed来辅助生成hash,有助于防止Hash DoS攻击。由于每次新建字符串时都需要计算一次hash,这个函数调用会比较频繁,因此它是一个稀疏的hash函数,内部实现使用了lookup3 hash算法,只会取字符串中的常数个字节进行计算,所以函数的运行是一个常数的时间。

得到hash值之后,根据当前字符串hash数组的大小,就能得到索引值。luajit为了速度考虑,实际保存的是mask,也就是hash数组的大小减一,数组大小永远是2的幂,那么mask就可以作为掩码,通过按位与就能迅速算出索引。luajit初始化时默认的hash数组大小为256。

// lj_def.h
#define LJ_MIN_STRTAB	256		/* Min. string table size (pow2). */

hash数组存放的是字符串链表,需要从链表头开始遍历,看看当前的字符串是否已经内化过了。这里的比较逻辑也是有讲究的,首先比较的是hash值和长度,只有两者都相等时,再调用更加费时的memcmp函数进行精确比较,这也是最大化地提升了比较的效率。

如果找到了相同的字符串,那么直接返回即可,如果没有,则需要调用lj_str_alloc函数进行真正的字符串创建。

// lj_str.c
static GCstr *lj_str_alloc(lua_State *L, const char *str, MSize len,
			   StrHash hash, int hashalg)
{
  GCstr *s = lj_mem_newt(L, lj_str_size(len), GCstr);
  global_State *g = G(L);
  uintptr_t u;
  newwhite(g, s);
  s->gct = ~LJ_TSTR;
  s->len = len;
  s->hash = hash;
#ifndef STRID_RESEED_INTERVAL
  s->sid = g->str.id++;
#elif STRID_RESEED_INTERVAL
  if (!g->str.idreseed--) {
    uint64_t r = lj_prng_u64(&g->prng);
    g->str.id = (StrID)r;
    g->str.idreseed = (uint8_t)(r >> (64 - STRID_RESEED_INTERVAL));
  }
  s->sid = g->str.id++;
#else
  s->sid = (StrID)lj_prng_u64(&g->prng);
#endif
  s->reserved = 0;
  s->hashalg = (uint8_t)hashalg;
  /* Clear last 4 bytes of allocated memory. Implies zero-termination, too. */
  *(uint32_t *)(strdatawr(s)+(len & ~(MSize)3)) = 0;
  memcpy(strdatawr(s), str, len);
  /* Add to string hash table. */
  hash &= g->str.mask;
  u = gcrefu(g->str.tab[hash]);
  setgcrefp(s->nextgc, (u & ~(uintptr_t)1));
  /* NOBARRIER: The string table is a GC root. */
  setgcrefp(g->str.tab[hash], ((uintptr_t)s | (u & 1)));
  if (g->str.num++ > g->str.mask)  /* Allow a 100% load factor. */
    lj_str_resize(L, (g->str.mask<<1)+1);  /* Grow string table. */
  return s;  /* Return newly interned string. */
}

这里主要可以分为两部分,一是创建填充GCstr数据结构,二是将GCstr链接到全局hash数组的正确位置上。GCstr有一个sid字段,它表示字符串的唯一ID,所有字符串都是不同的,生成方式与STRID_RESEED_INTERVAL宏定义有关。这个宏表示使用伪随机函数重新生成一个字符串id的间隔。

// lj_str.c
/* Reseed String ID from PRNG after random interval < 2^bits. */
#if LUAJIT_SECURITY_STRID == 1
#define STRID_RESEED_INTERVAL	8
#elif LUAJIT_SECURITY_STRID == 2
#define STRID_RESEED_INTERVAL	4
#elif LUAJIT_SECURITY_STRID >= 3
#define STRID_RESEED_INTERVAL	0
#endif

这个宏的值取决于LUAJIT_SECURITY_STRID宏的定义。

// lj_arch.h
#ifndef LUAJIT_SECURITY_STRID
/* String IDs: 0 = linear, 1 = reseed < 255, 2 = reseed < 15, 3 = random. */
#define LUAJIT_SECURITY_STRID	1
#endif

通过代码注释得知,sid的生成有4种模式,当LUAJIT_SECURITY_STRID定义为0时,sid是以线性递增的方式生成的,这样速度快,但是安全性较低;当定义为3时,sid则是以随机的方式生成的,每次都会调用伪随机函数得到新的sid,安全性高,但相应速度也会慢;当定义为1或2时,则以不同的间隔随机生成sid,随机数生成时,取二进制位前STRID_RESEED_INTERVAL位,记录在全局的idreseed字段上,作为下次调用伪随机函数的间隔。这样既保证了一定的安全性,又没有牺牲太多的性能,算是一个折衷的方案。伪随机数生成的函数名为lj_prng_u64,它是一个周期为 2 223 2^{223} 2223​的函数,因此基本不用担心随机数会生成重复。

当然,字符串内容也需要填充到GCstr中。实际分配GCstr的内存块大小,包含了GCstr和字符串,还有对齐字节:

// lj_str.h
#define lj_str_size(len)	(sizeof(GCstr) + (((len)+4) & ~(MSize)3))

在LJ_GC64模式下,GCstr本身所占的内存大小为24字节,已经是4字节对齐了:

LuaJIT源码分析(三)字符串3

那么,这里的对齐字节是为了让字符串对齐使用的,而且是向上取整,意味着如果len的大小已经是4字节对齐了,还是会多分配4个字节。之所以这么做,是为了快速填充字符串结尾的’\0’,它是不包含在字符串的长度中,但是又是实际需要填充进来的字符。这样luajit就可以快速地将最后4个字节先清零,然后再进行字符串拷贝,保证字符串是以’\0’结尾。

GCstr构造完毕后就要把它链接到hash数组中,这一步其实很简单,根据GCstr的hash值,与mask按位与得到数组的索引,然后作为链表的头部插入到链表中。自此,理想情况下字符串的构建工作就完成了。现在,让我们回过头来想一想不理想的情况。

前面说过,luajit为了快速计算字符串的hash值,使用的是一种稀疏的算法,只取了字符串的某些位来做hash运算。那么不可避免地,如果有大量相似的字符串,会导致明显的hash冲突。luajit会检测生成字符串时hash冲突的数量,也就是相同hash值下链表的长度,如果长度超过LJ_STR_MAXCOLL,就会把这个链表上的所有字符串重新hash一次,二次映射到不同的hash值上,以解决hash碰撞。

// lj_str.c
#define LJ_STR_MAXCOLL		32

rehash的函数名为lj_str_rehash_chain

// lj_str.c
/* Rehash and rechain all strings in a chain. */
static LJ_NOINLINE GCstr *lj_str_rehash_chain(lua_State *L, StrHash hashc,
					      const char *str, MSize len)
{
  global_State *g = G(L);
  int ow = g->gc.state == GCSsweepstring ? otherwhite(g) : 0;  /* Sweeping? */
  GCRef *strtab = g->str.tab;
  MSize strmask = g->str.mask;
  GCobj *o = gcref(strtab[hashc & strmask]);
  setgcrefp(strtab[hashc & strmask], (void *)((uintptr_t)1));
  g->str.second = 1;
  while (o) {
    uintptr_t u;
    GCobj *next = gcnext(o);
    GCstr *s = gco2str(o);
    StrHash hash;
    if (ow) {  /* Must sweep while rechaining. */
      if (((o->gch.marked ^ LJ_GC_WHITES) & ow)) {  /* String alive? */
	lj_assertG(!isdead(g, o) || (o->gch.marked & LJ_GC_FIXED),
		   "sweep of undead string");
	makewhite(g, o);
      } else {  /* Free dead string. */
	lj_assertG(isdead(g, o) || ow == LJ_GC_SFIXED,
		   "sweep of unlive string");
	lj_str_free(g, s);
	o = next;
	continue;
      }
    }
    hash = s->hash;
    if (!s->hashalg) {  /* Rehash with secondary hash. */
      hash = hash_dense(g->str.seed, hash, strdata(s), s->len);
      s->hash = hash;
      s->hashalg = 1;
    }
    /* Rechain. */
    hash &= strmask;
    u = gcrefu(strtab[hash]);
    setgcrefp(o->gch.nextgc, (u & ~(uintptr_t)1));
    setgcrefp(strtab[hash], ((uintptr_t)o | (u & 1)));
    o = next;
  }
  /* Try to insert the pending string again. */
  return lj_str_new(L, str, len);
}

对于需要重新hash的链表,luajit会把链表头的地址设置为(uintptr_t)1,作为二次hash的标记,同时设置全局的second字段为1,表示luajit启用了二次hash。然后开始遍历原来的链表,对于没有引用需要gc清理的字符串,会直接进行回收。存活的字符串,如果已经二次hash过了,依旧是留在原来的链表中,它的hash值保持不变;否则调用hash_dense函数重新计算hash。hash_dense函数会用到字符串的所有数据计算,所以它是线性的运行时间,要比之前的hash_sparse运行时间要慢,但是生成的hash值质量会更高,更难发生hash碰撞。

无论最后字符串的hash值是否发生变化,都需要重新做一次链接操作,也就是将字符串插入到对应hash值的链表头部。链表头部可能包含二次hash的特殊标记,所以还要插入的同时,把这个标记一直保留在链表头部。这里的实现方式很巧妙,首先是取出当前的链表头,然后通过u & ~(uintptr_t)1得到字符串对象地址,再通过u & 1得到标记本身,这样就能把标记转移到新的链表头部,以及清理掉旧链表头的标记。那么,这样就有一个疑问了,luajit如何能保证字符串的地址不会被标记所污染呢?这是因为luajit在进行内存分配时,是默认8字节对齐的,因此分配返回的地址,末3位均为0,可以拿来做一些附加的工作。

// lj_alloc.c
#define MALLOC_ALIGNMENT	((size_t)8U)

我们可以运行时通过断点来进行验证:

LuaJIT源码分析(三)字符串4

加入了二次hash之后,查找字符串是否已被内化的逻辑,也需要进行修改。在使用hash_sparse计算出字符串的原始hash值之后,还需要判断是否需要再次hash。检查对应索引下的链表头是否带有特殊标记,如有则使用hash_dense计算新的hash值,取出字符串真正所在的链表头。

// lj_str.c
if (LJ_UNLIKELY((uintptr_t)o & 1)) {  /* Secondary hash for this chain? */
  hashalg = 1;
  hash = hash_dense(g->str.seed, hash, str, len);
  o = (GCobj *)(gcrefu(g->str.tab[hash & g->str.mask]) & ~(uintptr_t)1);
}

理想情况下,hash数组每一项都只存放一个字符串,也就是不存在hash冲突。luajit会检测当前系统中的字符串数量,如果超过hash数组的长度,则会进行2倍扩容。调整hash数组大小的函数为lj_str_resize

// lj_str.c
/* Resize the string interning hash table (grow and shrink). */
void lj_str_resize(lua_State *L, MSize newmask)
{
  global_State *g = G(L);
  GCRef *newtab, *oldtab = g->str.tab;
  MSize i;

  /* No resizing during GC traversal or if already too big. */
  if (g->gc.state == GCSsweepstring || newmask >= LJ_MAX_STRTAB-1)
    return;

  newtab = lj_mem_newvec(L, newmask+1, GCRef);
  memset(newtab, 0, (newmask+1)*sizeof(GCRef));

#if LUAJIT_SECURITY_STRHASH
  /* Check which chains need secondary hashes. */
  if (g->str.second) {
    int newsecond = 0;
    /* Compute primary chain lengths. */
    for (i = g->str.mask; i != ~(MSize)0; i--) {
      GCobj *o = (GCobj *)(gcrefu(oldtab[i]) & ~(uintptr_t)1);
      while (o) {
	GCstr *s = gco2str(o);
	MSize hash = s->hashalg ? hash_sparse(g->str.seed, strdata(s), s->len) :
				  s->hash;
	hash &= newmask;
	setgcrefp(newtab[hash], gcrefu(newtab[hash]) + 1);
	o = gcnext(o);
      }
    }
    /* Mark secondary chains. */
    for (i = newmask; i != ~(MSize)0; i--) {
      int secondary = gcrefu(newtab[i]) > LJ_STR_MAXCOLL;
      newsecond |= secondary;
      setgcrefp(newtab[i], secondary);
    }
    g->str.second = newsecond;
  }
#endif

  /* Reinsert all strings from the old table into the new table. */
  for (i = g->str.mask; i != ~(MSize)0; i--) {
    GCobj *o = (GCobj *)(gcrefu(oldtab[i]) & ~(uintptr_t)1);
    while (o) {
      GCobj *next = gcnext(o);
      GCstr *s = gco2str(o);
      MSize hash = s->hash;
#if LUAJIT_SECURITY_STRHASH
      uintptr_t u;
      if (LJ_LIKELY(!s->hashalg)) {  /* String hashed with primary hash. */
	hash &= newmask;
	u = gcrefu(newtab[hash]);
	if (LJ_UNLIKELY(u & 1)) {  /* Switch string to secondary hash. */
	  s->hash = hash = hash_dense(g->str.seed, s->hash, strdata(s), s->len);
	  s->hashalg = 1;
	  hash &= newmask;
	  u = gcrefu(newtab[hash]);
	}
      } else {  /* String hashed with secondary hash. */
	MSize shash = hash_sparse(g->str.seed, strdata(s), s->len);
	u = gcrefu(newtab[shash & newmask]);
	if (u & 1) {
	  hash &= newmask;
	  u = gcrefu(newtab[hash]);
	} else {  /* Revert string back to primary hash. */
	  s->hash = shash;
	  s->hashalg = 0;
	  hash = (shash & newmask);
	}
      }
      /* NOBARRIER: The string table is a GC root. */
      setgcrefp(o->gch.nextgc, (u & ~(uintptr_t)1));
      setgcrefp(newtab[hash], ((uintptr_t)o | (u & 1)));
#else
      hash &= newmask;
      /* NOBARRIER: The string table is a GC root. */
      setgcrefr(o->gch.nextgc, newtab[hash]);
      setgcref(newtab[hash], o);
#endif
      o = next;
    }
  }

  /* Free old table and replace with new table. */
  lj_str_freetab(g);
  g->str.tab = newtab;
  g->str.mask = newmask;
}

所谓的resize,就是重新分配一个指定大小的hash数组,然后把原来hash数组里的所有字符串链接到新的hash数组上去。不过由于有二次hash的存在,事情变得稍微复杂了些。首先,原先需要二次hash的链表,在resize之后,不一定还需要二次hash了。luajit判断链表需要二次hash的标准是,链表的长度超过LJ_STR_MAXCOLL,所以在新的hash数组中,luajit统计了每个索引的链表长度。特别注意一点,如果原链表中的字符串是二次hash的,那需要重新计算一下它的原始hash值。统计完毕后,luajit遍历新的hash数组,如果某个索引下的链表长度超过了LJ_STR_MAXCOLL,需要重新hash,则会将它的链表头设置为1,否则为0。这里的1就是前面所说的特殊标记。

准备工作完成后,就可以把所有的字符串链接到新的hash数组中。链接分为以下4种情况:

  1. 字符串没有二次hash过,链表不需要二次hash;
  2. 字符串没有二次hash过,链表需要二次hash;
  3. 字符串二次hash过,链表不需要二次hash;
  4. 字符串二次hash过,链表需要二次hash。

第一种情况,直接插入即可:

LuaJIT源码分析(三)字符串5

第二种情况,需要计算字符串的二次hash值,然后插入到正确的位置,同时需要修改字符串的hash值为二次hash,以及二次hash的标记为1:

LuaJIT源码分析(三)字符串6

第三种情况,需要计算字符串的原始hash值,然后插入到正确的位置,同时需要修改字符串的hash值为原始hash,以及二次hash的标记为0:

LuaJIT源码分析(三)字符串7

最后一种情况,直接插入即可。

LuaJIT源码分析(三)字符串8

自此,我们算是把luajit中字符串的数据结构,内存管理的内容梳理完毕了。通过一系列的分析可以看出,luajit在字符串的执行效率上和内存占用上下了很大的功夫,做了很多的考虑。我们从中也能看到两个比较明显的性能热点,一是hash冲突导致的rehash过程,这个会让链表上的所有字符串重新hash;二是hash数组大小不够导致的resize过程,这个需要把所有的字符串重新链接。

Reference

[1] Luajit String Interning

[2] Speeding Up Strings

[3] Lua string 哈希碰撞

[4] Lua string hash 算法

[5] Reduce string hash collisions

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenNJet产品体验-手把手在Ubuntu20.04系统从零部署到应用OpenNJet

目录 一、引言 二、OpenNJet产品安装 2.1下载OpenNJet安装包 2.2安装OpenNJet V2.0.1 ​2.3快速启动并测试OpenNJet 三、OpenNJet产品应用体验 3.1配置OpenNJet 3.2 部署 Web 应用程序 3.3启动 NJet 3.4访问 Web 应用程序 四、总结 一、引言 OpenNJet应用引擎是高性…

代码随想录算法训练营DAY47|C++动态规划Part8|198.打家劫舍、213.打家劫舍II、198.打家劫舍III

文章目录 198.打家劫舍思路CPP代码 ⭐️213.打家劫舍II解决环的问题思路总结CPP代码 ⭐️198.打家劫舍III思路递归三部曲——确定参数和返回值递归三部曲——确定终止条件递归三部曲——确定单层遍历的逻辑 打印dp数组CPP代码暴力递归记忆化递归 198.打家劫舍 力扣题目链接 文章…

小米手机怎么截屏?一篇文章教会你!

我们经常需要截取手机屏幕上的内容&#xff0c;无论是为了分享、教学还是记录。对于小米手机用户来说&#xff0c;掌握截屏技巧不仅能够方便地记录精彩瞬间&#xff0c;还能在需要时轻松分享或保存屏幕内容。小米手机怎么截屏&#xff0c;有哪些截屏方式呢&#xff1f;本文将为…

Remix中使用 Tailwind

如何Remix 中使用 Tailwindcss Remix是通过 Vite 进行构建的&#xff0c;Vite 原生就支持 PostCSS&#xff0c;通过以下几步就可以设置完成 安装 PostCSS 插件 npm install -D tailwindcss autoprefixer添加PostCSS配置文件 ## postcss.config.js export default {plugins:…

深度学习网络:设计、开发和部署

​书籍&#xff1a;Deep Learning Networks: Design, Development and Deployment 作者&#xff1a;Jayakumar Singaram&#xff0c;S. S. Iyengar&#xff0c;Azad M. Madni 出版&#xff1a;Springer书籍下载-《​深度学习网络&#xff1a;设计、开发和部署》该教材为学生和工…

cesium雷达遮罩(轨迹球效果)

cesium 雷达遮罩(轨迹球效果) 以下为源码直接复制可用 1、实现思路 通过修改“material”材质来实现轨迹球效果 2、代码示例 2.1 index.html <!DOCTYPE html> <html lang="en"><head><!

【刷题篇】双指针(二)

文章目录 1、有效三角形的个数2、查找总价格为目标值的两个商品3、三数之和4、四数之和 1、有效三角形的个数 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 class Solution { public:int triangleNumber(vector<int>& n…

u盘数据突然不见了怎么回事 u盘数据为什么无故不见如何恢复

当我们把文档资料保存好并复制到U盘后&#xff0c;发现刚才保存的资料没了&#xff0c;并且连文档都不见了&#xff0c;不过文件夹还在U盘。那么U盘数据突然不见了怎么回事呢?U盘数据突然不见了怎么办? 一、U盘数据突然不见了怎么回事 有许多原因可能导致u盘数据无故消失&a…

伪装目标检测论文阅读之Dual-SAM(CVPR-2024)

论文&#xff1a;link code&#xff1a;code Fantastic Animals and Where to Find Them:Segment Any Marine Animal with Dual SAM 摘要 作为水下智能的重要支柱&#xff0c;海洋动物分割(MAS)涉及对海洋环境中的动物进行分割。以往的方法在提取长范围上下文特征方面表现不佳…

内存卡突然罢工?数据恢复有高招!

内存卡作为我们日常生活中常见的存储设备&#xff0c;广泛应用于手机、相机等设备中。然而&#xff0c;有时我们会遇到内存卡损坏打不开的情况&#xff0c;这时该如何应对呢&#xff1f;本文将为您详细解析内存卡损坏的原因&#xff0c;并提供有效的数据恢复方案&#xff0c;帮…

PyQt5的基本安装与使用

文章目录 1. 简介2.安装2.1.QtDisigner配置2.2 PyUIC配置2.3. PyRCC配置 3. 一个简单的PyQt5使用示例 1. 简介 PyQt5是一个用于创建交互式界面的Python库&#xff0c;它是基于Qt框架的Python绑定。Qt是一个跨平台的C框架&#xff0c;用于开发图形用户界面&#xff08;GUI&…

4.26.7具有超级令牌采样功能的 Vision Transformer

Vision Transformer在捕获浅层的局部特征时可能会受到高冗余的影响。 在神经网络的早期阶段获得高效且有效的全局上下文建模&#xff1a; ①从超像素的设计中汲取灵感&#xff0c;减少了后续处理中图像基元的数量&#xff0c;并将超级令牌引入到Vision Transformer中。 超像素…

源代码加密的重要性

在数字化时代&#xff0c;企业面临的最大挑战之一是如何保护其核心数据不被泄露。企业源代码防泄密是指企业采取措施保护其软件或应用程序源代码不被未授权的人员获取、泄露或盗用的一种安全措施。源代码是软件的核心组成部分&#xff0c;其中包含了程序员编写的具体指令和算法…

MySQL深入理解MVCC机制(详解)

深入理解MVCC 1、MVCC定义 MVCC:Multi-Version Concurrency Control&#xff0c;多版本并发控制机制。 在mysql中&#xff0c;为了满足事务的四大特性之一的隔离性&#xff0c;就是当前事务中的查询的数据不受其他事务的增删改操作的影响&#xff0c;因此mysql主要是通过这个…

怎么解决端口被占用

目录 一、引言 二、解决方法 一、引言 最近用vscode写网页&#xff0c;老是遇见端口被占用&#xff0c;报错如下&#xff1a; listen tcp :8080: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted. 二、解决方法 1.换…

Apache DolphinScheduler 3.3.0 版本重磅更新提前看!

Apache DolphinScheduler 3.3.0版本终于要在万众期待中发布啦&#xff01;本次发版将有重大功能更新&#xff0c;包括架构上的调整。 为了让广大用户提前尝鲜&#xff0c;社区特别准备了直播活动提前揭秘3.3.0版本中的重要更新&#xff0c;到时候你将会了解到这些信息&#xf…

苹果平板HOME键成历史,全面屏时代到来?2024平板电脑市场趋势分析

近期苹果公司在“放飞吧”发布会上推出了新款iPad Pro和iPad Air平板电脑&#xff0c;并下架了最后一款带有实体Home按键的iPad 9。这一变化标志着Home键在苹果iPad产品线中成为了历史&#xff0c;引起了不少网友的怀念和感慨。 与此同时&#xff0c;今年3月线上平板电脑市场迎…

分析:Palo Alto在从SASE向SASO演进中定位不佳

摘要 我们通过上一篇文章&#xff08;Fortinet的愿景——超越SASE&#xff09;中应用于Fortinet的相同框架来回顾Palo Alto Network在网络和网络安全方面的前景。 SASE涉及数据传输的第一英里。不过&#xff0c;随着SASE的发展&#xff0c;投资者还需要考虑中间和最后一英里。…

QLabel 如何同时显示图片和文字?

效果: align="top"表示图片和文字底部对齐。 img src=":/img/qrc_img.png"表示此图片被添加到qrc的相对路径。 完整: QString content =QString("<html><head/><body><p><img src=\":/img/qrc_img.png\"…

【linux kernel】linux内核hid触摸源码hid-multitouch.c剖析

文章目录 一、内核中通用hid触摸驱动二、probe过程剖析(1)hid_parse()函数(2)hid_hw_start()函数(3)hid_connect()函数三、hid-multitouch.c应用场景一、内核中通用hid触摸驱动 在linux内核中,为HID触摸面板实现了一个通用的驱动程序,位于/drivers/hid/hid-multitouch.c文件…
最新文章