【论文笔记】Persistent Memory Hash Indexes: An Experimental Evaluation

 

目录

Abstract

1 INTRODUCTION

2 BACKGROUND

2.1 Optane DC Persistent Memory

2.2 Programming Persistent Memory

3 PERSISTENT HASH TABLES

3.1 Level Hashing and Clevel

3.2 CCEH

3.3 Dynamic and Scalable Hashing (Dash)

3.4 PCLHT

3.5 SOFT

3.6 Summary

4 EXPERIMENTAL EVALUATION

4.1 Environment and Setup

4.2 Single-threaded Performance

4.3 Multi-threaded Performance

4.4 DCPMM Scalability

4.5 Latency

4.6 Resizing

4.7 Load Factor

4.8 Memory Utilization

4.9 Recovery

4.10 Breakdown of Data Written to PM

4.11 Issues Related to PM Hardware

4.12 Impact of NUMA

5 DISCUSSIONS

6 RELATED WORK

7 CONCLUSION


Abstract

         持久化存储Persistent memory (PM)越来越多的被用于构建基于哈希的索引结构,并且有着低成本持久化、高性能和及时恢复等特性,特别是随着最近 Intel Optane DC Persistent Memory Modules(DCPMM)的发布,PM越来越火热。然而,大部分PM是在基于DRAM的仿真器emulators进行的评估,并且假设unreal,专注于特定的指标而越过了重要的特性。因此,理解最近提出的哈希索引在真实的PM上表现如何,以及在不同性能标准下比较不同索引技术之间的区别很重要。

         为此,本文对持久化哈希表persistent hash tables提供详细的测评。在真实的PM硬件上测试6种最新的哈希表技术,包括Level hashing, CCEH, Dash, PCLHT, Clevel, SOFT。评估实验采用统一的测评框架和代表性的workload。除了常见的性能指标,也探索了不同硬件配置(PM带宽、CPU指令和NUMA如何影响基于PM的哈希表性能。有了深入的研究,我们指出设计的trade-off,以及较好的范例,以及基于PM的哈希表未来可能的优化方向

(在真实PM上对哈希表技术进行测评,给出建议)

1 INTRODUCTION

         目前已经提出了多种基于PM的不同物理介质。PM的特征有byte-addressability, direct persistence, DRAM-scale latency。3D XPoint是最有名的PM技术之一,基于这项技术的DCPMM是首款商用PM产品。最近,很多人从宏观与微观角度都对DCPMM进行了测评,对本文有所启发,但其中一些结果不一致。Yang发现DCPMM的真正表现比slower persistent DRAM label指示的要更复杂和有细微差别。在PM上的应用性能与access size, workload, pattern, degree of concurrency密切相关。与之前工作不同,Lersch发现PM带宽是稀缺资源,对性能有很重要的影响。有研究显示,end-to-end write latency通常比read latency更低。在设计PM数据结构时没有充分考虑PM硬件的特征。例如,基于不对等的读写性能的假设,有些方法设计的read更多,write更少,这是不符合实际的。(设计与真实特征不符)

         Scalable PM的出现有助于解决目前应用的问题,其中哈希索引结构/哈希表是许多软件系统的基础组件,能从PM的特征中受益,从而实现高性能和instant recovery。目前已经有许多面向PM设计的哈希表。他们要么基于DRAM emulation,要么是针对actual PM设计,要么从现有哈希表改进。这些方法在真实PM上表现如何,不同性能标准下有什么区别,在不同硬件配置下表现如何,目前仍不清楚。(缺少对哈希表的完善测评)

         因此,本文在配有DCPMM的系统上对6种哈希表技术进行详细评估,使用PiBench的扩展benchmarking framework,能提供一系列接口和统一的测试环境。这6种技术覆盖了不同维度的很多技术。实验使用典型的workload,得到的启发可以指导未来基于PM的哈希表设计。贡献总结如下:

1. 使用扩展后的PiBench框架对哈希表技术评估,所有实验结果公平可比较

2. 从宏观和微观角度评估基于PM的concurrent hash table,处理throughput, scalability, latency, load factor, LLC misses, resizing overhead, memory usage, recovery overhead,还考虑了PM硬件的影响,结合不同的CPU指令和NUMA框架。实验在真实PM上进行。

3. 对设计trade-off,范例,优化给出了建议和总结,能帮助未来基于PM的哈希表研究。

         第2章介绍DCPMM和Persistent Memory Development Kit(PMDK)的背景知识。第3章介绍6种哈希表技术的设计与实现。第4章介绍实验结果。第5章总结观察结果。第6章介绍相关工作,第7章给出结论。(评估平台统一、评估标准扩充、结论具有指导意义)

2 BACKGROUND

2.1 Optane DC Persistent Memory

DCPMM是首款商用PM产品,它通过在易失DRAM和基于块的存储之间创建new non-volatile tier新的非易失层,改变了传统的计算机内存层次。

Hardware Architecture. DCPMM是随着Cascade Lake架构提出的,which supports multiple sockets (2/4/8), each consisting of one or two processor dies that comprise separate NUMA nodes。集成的内存控制器iMC可以与DDR4和DCPMM DIMMs相连。为了保持数据持久性,iMCs位于asynchronous DRAM refresh (ADR) domain,保证到达这的CPU存储能免受power failure。iMC对每个DCPMM有读写pending queues (RPQs, WPQs),但是只有WPQs在ADR域中。因此,当数据到达WPQs,一旦有系统crash,数据会被iMC flush到PM中。由于3D-XPoint物理介质的存取粒度是256 bytes,on-DIMM控制器会把更小的请求转成256 bytes,由于小数据的存储变成了read-modify-write操作,造成了写方法。On-DIMM控制器有small write-combing buffer (XPBuffer) 来合并adjacent writes。与WPQs相似,XPBuffer存在于ADR域中,因此all updates are already persistent when they arrive at the XPBuffer.(介绍DCPMM硬件架构)

Operating modes. DCPMM可以有Memory模式和App Direct模式。在每个模式下,内存都可以interleaved across channels and DIMMs。2个模式都允许通过load/store指令进行PM的direct access。在Memory mode,DRAM作为最常获取数据的cache,DCPMM提供没有persistence特性的大量存储空间memory capacityDRAM补充)。在App Direct mode,应用程序和OS都知道有2种directly accessible memory。尽管PM是持久化的,CPU caches和registers都是volatile。当cacheline flush instruction,例如clflush, clflushopt, clwb指令执行,或者导致cacheline flush发生后,数据会在PM中持久化存储。为了保证系统崩溃后应用程序正确恢复,必须使用memory fences为避免指令重排。由于主要任务是评估性能,因此把DCPMM配置成App Direct mode。DCPMM两种模式,设置成App Direct

Performance. 尽管DCPMM为了direct and byte-addressable load/store access而设计,相比于DRAM,它有更低的带宽和更高的读写延迟。之前研究表明,DCPMM读延迟~300 ns,是DRAM的4倍。另外,DCPMM有非对称读写延迟,研究表明端到端写延迟as seen by the application比读延迟更低,因为当数据到达memory controller中的ADR区域后writes return。但是如果数据没有在RPQs中缓存,读很可能要访问DCPMM物理介质。最大的顺序读/写带宽是40GB/s和13GB/s,比DDR4 DRAM要慢3倍和11倍。对于随机读写,这种非对称带宽更加明显。DCPMM读写延迟/带宽非对称)

2.2 Programming Persistent Memory

         一方面,相比于需要serialization或者flushing到基于block的存储相比,PM能构建更的数据结构。另一方面,PM打破了传统的volatile memory和external memory之间的volatile-persistent boundary,也创建了process cachePM之间的new boundary。使得PM系统的编程更复杂,更易出错。例如,算法必须正确存储data,通过缓存到CPU cache或者使用non-temporal store and memory barrier来保证数据一致性consistency。然而,数据一致性需要存储顺序正确,并且数据永久存储。为了使存储(写大于8 bytes)是原子的,通常需要依赖特定的机制,例如redo/undo logging, copy-on-write, versioning, hybrid memory。这些机制复杂且容易出错。PM系统编程复杂)

         这些挑战可以通过利用PM编程库中的sound programming model来解决。为了尽可能与原先的哈希表实现保持一致,本文使用PMDK来map memory files, allocate PM memory, persist data,使用xfs文件系统(native DAX support: 可以直接获取PM backed files)来组织管理PM数据。(使用PMDK+xfs实验)

3 PERSISTENT HASH TABLES

         介绍Level hashing [2018], Clevel hashing [2020], CCEH [2019], Dash [2020], PCLHT [2019], and SOFT [2019],按照以下分类标准:① converted from DRAM counterparts or specifically designed for real PM hardware; 由DRAM对应结构转化还是针对PM硬件设计;② based on various structures like separate chaining, open addressing, or extendible hashing; 基于不同结构;③ using different synchronization mechanisms (lock-based or lock-free); 使用不同同步方法;④ using distinct resizing strategy (dynamic or static); 使用不同resizing策略;⑤ employing different crash-consistency mechanisms 使用不同的崩溃一致性机制 (不同分类标准)

3.1 Level Hashing and Clevel

       Level Hashing是针对PM的写优化、可扩展的哈希表,支持低成本的一致性保障和resizing。使用基于共享的2层结构,来实现常数级别的时间复杂度。图1(a)中,只有顶层bucket可寻址,底层的bucket用于存储从顶层收回的内容。类似于PCM友好的哈希表(PFHT,Level Hashing在每个bucket中有多个slots,在往full bucket中插入1个item时允许回收至多1个item。每个key可以从2个哈希计算得到的目标位置中选择,与Cuckoo哈希类似,从而提高load factor。(结构设计)

         Level hashing中,当resizing时,创建1个4倍大的哈希表,在底层的bucket中的key-value pairs首先重新哈希到新的表,然后删除旧的底层bucket。这时,新的表成为了顶层,之前的顶层成为了现在的底层。为了实现低成本一致性,在delete, insert, resize操作时通过每个bucket head的bitmaptoken,使用log-free方法。Level hashing在并发控制时依赖slot-grained lock,不维护指针,slot中保存真实KV。(扩容策略、一致性、并发控制)

         缺点:1. 静态哈希方法,重哈希代价高;2. 没有在真实PM评估。

         Clevel是基于Level hashing的lock-free并发哈希表,拥有动态多层结构,支持并发,resizing通过后台线程完成,不会阻塞并发查询。

3.2 CCEH

         基于可扩展hash设计来解决Level hashing的缺点,具有低成本动态内存管理常数级查找时间constant lookup time, 和failure-atomicity guarantee without explicit logging. 在传统的可扩展哈希中,维护一个directory,其中有保存bucket地址的指针。当bucket增加,directory可能消耗大量内存,因此不能reside in CPU cache。因此,CCEH提出在directory与bucket之间的中间层(segment),从而减少用于寻址bucket的指针数量。图1(b)中,directory 条目指向1个segment,其中有固定数量的bucket,通过哈希值的L个最低位LSBs进行索引,而segment是由G个最高位MSBs进行索引。通过把多个bucket放入segment中,可以减少directory的大小。(结构设计)

         当bucket满了,插入item导致冲突发生时,与可扩展哈希中split bucket的方法不同,CCEH splits segment从而扩大容量。然而,分裂segment会导致low load factor和更多的PM访问,因为segment中其他bucket可能仍有空闲的slots。因此,CCEH在split之前使用线性探测来提升空间利用率。当split segment完成后,一些item需要移动到新创建的segment。CCEH使用lazy deletion策略来保证旧segment中移动的item不需要立刻清除,因为这些item会被search操作忽略,并被insert操作重写,从而避免extra writes。(扩容策略)

3.3 Dynamic and Scalable Hashing (Dash)

         Dash是构建动态可扩展哈希表的整体方法,可以实现可扩展哈希Dash-EH和线性哈希Dash-LH. 与CCEH类似,Dash使用3层结构:directory, segment, bucket。图1(c)中,directory条目指向segment,其中包括64个常规bucket和2个stash bucket用于存储overflowed键值对。这两种bucket有相同的layout,每个bucket 256 bytes,在头部有32 byte metadata,然后有14个键值对。(结构设计)

         Dash使用balanced insert, displacement, and stashing 技术来解决CCEH的low load factor问题。在插入key时,首先利用key的哈希值计算segment index s和bucket index b,然后根据metadata中的alloc分配位图在bucket b/b+1中寻找empty slot。如果两个bucket都满了,会触发displacement操作来给新key分配空间。在metadata中维护了membership bitmap从而加速displacement。如果前2个操作都失败,key就插入stash bucket. 如果插入成功,更新原本bucket的overflow flag,否则分裂segment。最坏情况下,Dash需要探测bucket b/b+1, stash bucket,造成更多的cache misses and PM reads。在metadata中也保存指纹FP(key的1 byte哈希值)用于加速negative queries。 Dash使用乐观并发控制,对插入操作使用bucket-level CAS lock。搜索操作lock-free,不过仍需验证返回的key。(平衡插入,并发控制)

3.4 PCLHT

         Persistent Cache-Line Hash Table (PCLHT)是在RECIPE中讨论的chaining-based concurrent crash-consistent哈希表,是DRAM-based hash table CLHT的变体

         CLHT是cache友好的,因为bucket大小为64-byte (a common cacheline size). 每个bucket中有8 -byte word用于并发控制、next pointer和最多3个16-byte的键值对。通过保证每次哈希表更新在一般情况下只需要1次cacheline access,从而解决缓存一致性问题cache coherence problem。为了保证非阻塞读能得到正确结果,CLHT针对操作使用bucket的atomic snapshot。插入和删除操作都使用a single atomic commit point that is ordered by memory fences: 对插入操作,在更新8-byte key之前写入正确value;对删除操作,key写入0. 如果表满,插入失败,CLHT 使用copy-on-write方法来resize table。CLHT操作介绍)

         因为插入、删除、resize操作通过单一原子存储来实现,它们能通过在对应的store指令后插入cacheline flushes和memory fences,从而转化成对应的持久化形式。(加有序变成PCLHT

3.5 SOFT

         SOFT是为了避免数据结构中持久化指针而提出的chaining-based lock-free hash table。SOFT包括2个主要部分:persistent node (PNode) and volatile node (VNode)。PNode包括1个键值对和3个validity bits。VNode包括1个键值对和2个指针:1个指向具有相同键值对的PNode,另1个指向下一个VNode。只有PNode在PM中持久化,所有VNode存储在DRAM中,SOFT没有实现resize操作。当插入key,新的PNode和VNode被分配,VNode会通过CAS与现存的VNode连接。删除操作类似,只是节点被删除。SOFT可以通过扫描PM中的PNodes和重构DRAM中的结构来从系统崩溃中恢复。(链式结构,插入删除恢复操作)

3.6 Summary

在表1中总结这些经常用于PM哈希表设计的哈希表特征,包括数据结构、并发控制方法、内存架构、resizing方法。

Structure. CCEH和Dash使用基于directory-segment的结构,这种3层结构比传统的可扩展哈希更空间高效。Level hashing使用基于共享的2结构,提供常数级别复杂度的写constant-scale time complexity。PCLHT和SOFT是基于chaining的设计,使用指针来连接buckets或nodes。

Concurrency. 除了SOFT和Clevel,其他哈希表是基于锁的。Level hashing使用slot-grained locking, CCEH使用2层读写锁,PCLHT使用bucket-grained。Dash使用乐观锁机制,其中写由bucket-grained lock保护,读是lock-free。

Architecture. SOFT把volatile和persistent nodes分别存储在DRAM和PM。这种混合架构可能达到接近DRAM的性能,但是恢复时间长,依赖于DRAM中需要重建的结构大小。其他4种哈希表把数据存储在PM种,存在较长的读写延迟。

Resizing. CCEH和Dash都通过段分裂来扩展空间。段分裂比全表重新哈希需要更少的PM访问。另外,CCEH使用lazy deletion技术来减少段分裂的成本。PCLHT和Level hashing中,resizing操作会使哈希表大小翻倍,但是bucket-sharing结构能使得在resizing时重用2/3的buckets。Clevel的resizing操作lock-free。SOFT不支持resizing。

4 EXPERIMENTAL EVALUATION

4.1 Environment and Setup

Hardware Configuration.

CPULinux server (kernel version 5.0.0), 2个16核2.3GHz的Intel Xeon Gold 5218 CPUs,其中由32 hyperthreads, 32KB L1 instruction cache, 32KB L1 data cache, 1024KB L2 cache. The last level cache is 22MB in size.

内存系统:包括192GB of DDR4 DRAM and 768GB of Optane DCPMM (6 x 128 GB DCPMMs) configured in the App Direct mode. 对多线程性能和NUMA进行评估时使用2个CPU,其他情况使用1个CPU。

Parameters. 除了Level hashing,所用配置都与论文中一样。Level hashing中默认key和value的大小分别为16 bytes和15 bytes。因此a bucket with 3 slots and a 3-byte token can be aligned to 128 bytes (two cachelines).本文中使用8 bytes来存储键值对,因为哈希表只支持这个配置。因此Level hashing中的bucket大小为64 bytes。CCEH使用16KB segments和64-byte buckets(4 slots), and probes at most 4 cachelines (4 buckets, 16 slots, 256 bytes). Dash中a bucket has 14 slots that consume 256 bytes memory, and a segment contains 64 normal buckets and 2 stash buckets. PLCHT是基于chaining的,bucket is aligned to 64 bytes。SOFT也是基于chaining,每个node中有1对键值对,根据需要创建或删除node。

Implementation. 对于CCEH, Level, PCLHT, and SOFT,使用PMDK的libvmem库来管理PM内存。对于Dash,使用原有的实现,用PMDK中的libpmem and libpmemobj提供的接口。对于Clevel,也是用原有的实现,利用PMDK加上C++绑定。对于cacheline flush,为了更高效使用clwb指令。对于哈希函数,使用GCC的std::Hash_bytes,这个方法快速且能提供高质量的哈希。所有的哈希表都不允许重复keys。

Benchmark Framework. 由于容易使用和设计良好的架构,我们使用PiBench框架,并进行适当扩展。它可以通过触发常用操作来收集多种信息throughput, latency, and hardware counters using the Processor Counter Monitor (PCM) library through well-defined interfaces that can invoke common operations of indexing structures, such as insert, search, delete, and update etc. 我们扩展PiBench框架,使其能对其他功能进行评估:load factor, utilization, resize latency, recovery time, and the number of instructions targeting PM (cache flushing and memory fence) 为了评估特定哈希表,只需要实现PiBench定义的接口,封装到共享库中,从而被PiBench运行时加载。另外,我们使用的3个PiBench在workload generation提供的random distributions:uniform, self similar, and Zipfian

Workloads. 我们对不同workload的每种哈希表的单个操作 (insert, positive and negative search, delete)都进行了压力测试stress test。Negative search是指搜索不存在的key。除非另作说明,本文使用以下方法来产生workload。初始化哈希表其容量能容纳16M 键值对,SOFT除外,因为它是按需创建和产生,因此为其初始化16M head nodes,链表的头节点不用于数据存储。

         为了衡量insert-only性能,直接插入200M 记录到空哈希表中。为了衡量search、delete、mixed workloads的性能,首先初始化200M items的哈希表(loading phase),然后执行200M 次操作 (measuring phase)。与之前的研究类似,实验的workload包括uniform、skewed distribution (80%的访问20%的key.  self similar with a factor of 0.2, which means 80% of accesses focus on 20% of keys)。由于CCEH, Level hashing, PCLHT都不支持变长键值对,因此只考虑固定长度8-byte的键值对。

4.2 Single-threaded Performance

Search. 搜索是哈希表中最基本的操作,因为对哈希表更新都需要先确认某个key是否存在。PCLHT由于设计思想the search operation should not involve any stores, waiting or retries,具有最高的正向搜索性能。另外PCLHT在扩展容量时进行全表重哈希,能保证基于chaining的bucket足够小,从而使得在哈希表满时的搜索性能基本是constant time。我们在运行search-only的workload时收集the last level cache (LLC) misses。在图3(b)中,PCLHT的数量是最少的,平均每个search只需要读1.4 cacheline accesses。另外图3(a)中反映PCLHT读的数据也更少。(搜索正向搜索吞吐量TP最大,读的数据和LLC miss最少)

与之对比,SOFT需要遍历长的链式bucket,导致最大数量的LLC miss (see Figure 3(b)),但是SOFT是从存储在DRAM的VNodes中读取keys,没有PM读(see Figure 3(a))。另外发现SOFT的正向搜索比负向搜索吞吐量throughput更高(1.4x and 2.0x under uniform and skewed distribution respectively). 这是因为SOFT在负向搜索时需要遍历所有nodes,导致了更高的LLC miss (see Figure 3(b)).(正向搜索比负向搜索TP高,不读PMLLC miss最多)

Dash在metadata中维护了指纹数组,能帮助避免不必要的PM访问。由于这个优化,Dash在负向搜索时吞吐量最高。为了查找已存在的item,Dash最坏情况下需要probe 4 buckets (target bucket, neighbor bucket, and two stash buckets)。(负向搜索TP最高,LLC倒数第二)

而CCEH需要探查5连续的bucket。这说明Dash最坏情况下需要access 4 XPlines in the XPBuffer,而CCEH最多只需要access 2 XPlines。因此Dash从PM读的数据大约是CCEH的1.3倍(see Figure 3(a)),导致正向搜索的吞吐量略低于CCEH。(正向搜索TP第二)?

Clevel从PM读的数据最多,因为需要探查哈希表的所有层(b2t搜索,还可能重搜),并且dereference pointers后才能获取最终的键值对,因此Clevel的吞吐量最低。(正向搜索TP最低,读数据最多;负向搜索也有指纹优化,为什么最低)?

Insert. 进行插入记录时,首先查找哈希表,确保没有相同的key,因此插入操作的性能依赖于查找操作。

图2(a)中反映,insert-only + uniform workload下,SOFT和CCEH的吞吐量接近。如果剩余空间不够,插入操作会造成resizing。因此resizing代价对insert吞吐量有重要影响。

SOFT动态结构,插入快,CCEH3层结构,在skew时线性探测降低一点性能;SOFT是其他线程帮写,且写入PM的结构数据量少,插入写数据最少)

Level hashing和PCLHT的吞吐量更低,因为需要耗时的resizing操作,会产生大量的额外PM写。另外,由于Level hashing的查找性能更低,因此其吞吐量小于PCLHT。(中游)

至于CCEH和Dash,有相似的resizing和search性能,但是CCEH的吞吐量更高,因为需要的每次插入cacheline flushes更少(see Figure 3(b))、写入PM的数据更少(see Figure 3(a)). 由于skewed分布产生了重复的keys,这种情况下的吞吐量有点不同。Dash插入更复杂一点,TPCCEH低一点)

PCLHT的吞吐量大大提升,因为resizing代价降低(fewer keys to be rehashed)。Clevel的吞吐量最低,因为需要为新插入的item动态分配内存,并且为了保证crash consistency需要对每个item记录很长的log entry。(并发复杂TP最低)

Delete. 删除操作的性能也与查询性能有关,在删除前需要检查key是否存在。删除操作需要多次读,只需要1次写。因此,删除的性能与正向搜索趋势类似。

SOFT在删除key后释放node,链表的大小随着key删除不断缩减,要探查的nodes越少,SOFT的查询性能越高,因此其在uniform和skewed情况下吞吐量都最高。(最好)

PCLHT即使查询性能好,但删除性能仍比SOFT,这是因为PCLHT只是把key标记为invalid,包含已删除的keys的nodes仍然存在,导致搜索时额外的检查。在uniform下,Dash, CCEH, SOFT的性能差不多。由于查询效率低,Clevel的删除吞吐量也很低。(最差)

正向搜索:PCLHT > CCEH > Dash > SOFT > Level > Clevel

负向搜索:Dash > PCLHT > CCEH > Level > SOFT > Clevel

插入:    CCEH ~ SOFT > Dash ~ PCLHT > Level > Clevel

删除:    SOFT > Dash > CCEH > PCLHT > Level > Clevel  (吞吐量从高到低) 

4.3 Multi-threaded Performance

         与第4.2节配置一样, write heavy: 80% inserts and 20% searches。Balanced: 50% inserts 和50% searches。Read heavy: 20% inserts and 80% searches。

Search. 从图4(b)(c)(f)(g)中可以看出,当线程数<32时,所有哈希表scale well。PCLHT表现最好,与单线程性能相符。对于skewed下的负向搜索,PCLHT性能很好,在64个线程下达到60Mops/s的吞吐量,1.2x/2.2x/3.1x/4.3/7.1x of Dash/SOFT/Level/CCEH/Clevel。PCLHT正向搜索最好)

SOFT是动态lock-free哈希方法,搜索操作只需要访问DRAM,保证其性能较好。然而,SOFT也使用指针来连接节点,不会resize哈希表。因此,链表长度会限制搜索吞吐量。SOFT处于中上游)

对于Dash,threads can proceed without holding any locks for reads,使得其可扩展性较好。Dash使用的指纹技术能提升负向搜索的吞吐量,因为可以filter negative lookups to PM。因此,在uniform情况下,Dash比PCLHT的负向搜索性能要高(see Figure 4(c)). 在skewed情况下,PCLHT比Dash的负向搜索性能稍微好一点,因为访问的数据更少(20%)。(负向搜索PCLHTDash差不多)

CCEH比Dash性能低,因为即使对读操作也使用了悲观锁。通常来说,非阻塞读的哈希表能获得更高的搜索性能。CCEH中下游,最后是LevelClevel

Insert. SOFT比其他方法的性能更高。图4(a)(e)中显示,在线程数达到32时,uniform和skewed下的吞吐量分别时Level hashing的14.7倍和13.7倍。SOFT的高性能有4个原因:① Lock-free ② 在数据结构中完全避免保存任何指针 ③ 使用最少指令least amount of flush instructions (Figure 3(b)) ④ 每个操作的指令数接近与理论最小值 The number of fence instructions per operation is close to the theoretical minimum (Figure 3(b))。SOFT最好,动态)

Dash需要在bucket之间移动item以解决冲突,从而造成了更多的flush和写,其吞吐量略小于CCEH。(两个中游)

Level hashing吞吐量最低,因为需要resizing时整个表被锁住,另外还需要在resizing过程中删除bucket中的item,从而导致额外cache flush和PM写。(最差)

类似的,PCLHT的插入性能也很低,这是因为resizing代价(the second-largest overhead detailed in Section 4.6)。Clevel继承Level hashing的设计,即使在resizing时有lock-free优化,其性能也差不多。(插入多重构,性能也很低)

Delete. 图4(d)(h)中反映,与插入类似,SOFT表现最好。从图2中看出PCLHT比CCEH和Dash的单线程性能要略低,但多线程时要好,因为删除操作包括读和写,PCLHT的读更少,写与CCEH和Dash可比较,因此其bandwidth utilization更加高效。(SOFT>PCLHT> Dash >CCEH)

Mixed. 为了衡量mixed workload下的性能,初始化200M 记录的哈希表,执行200M次不同读写比例的操作。PCLHT性能最好。与insert-only相比,PCLHT在write heavy情况下吞吐量更高。因为在测评前插入了200M记录,free bucket是充足的,且不会触发resizing。当read heavy时,在uniform下PCLHT的吞吐量从19Mops/s to 32Mops/s steadily,在skewed下达到大概47Mops/s. CCEH, Dash, SOFT在write heavy和balanced下趋势相似。

         大部分哈希表在32个线程时吞吐量最大。但是,① SOFT在delete-only情况下吞吐量持续增加,因为SOFT查找DRAM中的keys,避免了PM的读访问,另外图3(a)中也显示了SOFT触发PM的写更少。 ② PCLHT does not level off until 64 threads under mixed workload. 因为PCLHT相比于其他方法的读写操作更少,在相同带宽限制下能执行更多操作。

结论: 线程数小于32时,随线程个数增加,吞吐量增大。

正向搜索:PCLHT > Dash ~ SOFT > CCEH > Level > Clevel

负向搜索:Dash ~ PCLHT > CCEH ~ SOFT > Level > Clevel

插入:    SOFT > CCEH ~ Dash > PCLHT > Level > Clevel

删除:    SOFT> PCLHT > Dash > CCEH > Level > Clevel

Mixed:    PCLHT > Dash ~ CCEH ~ SOFT > Level ~ Clevel  (吞吐量从高到低)

4.4 DCPMM Scalability

         为了评估不同DCPMM个数导致的吞吐量变化情况,重新运行第4.3节中1个DCPMM的多线程测评。定义吞吐率throughput ratio:使用6个DCPMM(interleaved)的吞吐量/单个DCPMM的吞吐量,用于反映DCPMM可扩展性。更高的比率反映更好的可扩展性,最理想的值是6。图6反映不同线程数下的结果,CCEH和Dash在more threads stressing the PM subsystem时吞吐率更高,说明通过增加DCPMM个数可以增加吞吐量。

         但是,即使在很多线程情况下DCPMM数量多不一定能提升吞吐量,Level hashing和SOFT就是这种情况。对于insert-only和mixed,Level hashing的吞吐率基本为1,在正向搜索时最大ratio也小于2。SOFT是把部分压力交给DRAM的混合内存设计,其吞吐量对DCPMM个数不太敏感。对于搜索,不管多少线程SOFT的吞吐率始终大概为1.

         总之,对于需要PM带宽的哈希表,分配更多DCPMM能有效提升性能。对其他哈希表,we observe only marginal returns with additional DCPMMs installed。对于SOFT这种混合内存设计,只需要配置正确的DCPMM数量,把更多DIMM slots留给DRAM更好,因为DRAM的带宽和容量可能对整体性能贡献更高。多少DCPMM和多少线程这里不做讨论。

         可扩展性从好到坏:CCEH ~ Dash > Clevel ~ PCLHT > Level ~ SOFT

4.5 Latency

         大规模系统suffer from unpredictable high percentile (tail) latency variations. 尾延迟反映了大部分操作的响应时间,但是有些设计会牺牲尾延迟来获得更高的吞吐量。我们收集uniform下的实验结果,从而排除caching影响和获得更稳定的access pattern。分析了单线程的尾时延,但是没有获得其他启发。因此只在图7中画出多线程的实验结果。

Insert. 除了SOFT所有哈希表都有高延迟,因为SOFT采用lock-free并发控制方法和混合内存架构。the tail latencies of CCEH, Level, Dash, Clevel, PCLHT and SOFT increase to milliseconds (about 1.6ms, 0.2ms, 2.3ms, 46ms, 0.7ms and 0.02ms respectively) at 99.999 percentile。最差情况下,每个方法的延迟大概为8ms, 58751ms (resizing triggered), 16ms, 325ms, 31134ms (resizing triggered), and 42ms。同时,CCEH和Dash的尾延迟超过其他方法,因为它们有结构性修改(段分裂)。 

Search. 搜索的尾延迟更依赖于并发控制方法。因此,CCEH有最高的正向延迟,因为使用了悲观锁。对于负向搜索,延迟基本固定,因为负向搜索需要遍历所有bucket。每个方法CCEH, Level, Dash, Clevel, PCLHT and SOFT的正向/负向搜索延迟in the scale of several microseconds (about 18/18µs, 10/12µs, 7/7µs, 18/60µs, 6/7µs, and 9/10µs respectively) at 99.999 percentile。

Delete. 删除操作可能造成多次读。因此,其延迟与搜索代价紧密相关。SOFT的延迟最低,因为在删除记录后清除目标node,使得chaining-based buckets更短。其他方法的延迟为CCEH, Level, Dash, Clevel, PCLHT and SOFT are tens of microseconds (about 34µs, 26µs, 58µs, 67µs, 23µs and 18µs respectively) at 99.999 percentile

多线程插入 SOFT > Level > PCLHT > CCEH > Dash > Clevel

搜索           PCLHT ~ Dash > SOFT > Level > CCEH ~ Clevel

删除       SOFT > PCLHT > Level > CCEH > Dash > Clevel (尾延迟从低到高)

4.6 Resizing

         对于需要动态容量扩展的哈希表来说resizing是不可缺少的。使用第4.1节中的insert workload对resizing进行实验。图8中反映,resizing的代价不应被忽略。CCEH为了扩展容量需要两个操作:段分裂的代价大约恒定是37.9𝜇s,与哈希表大小无关;directory doubling的代价随表大小而变化。当插入112M items来触发这个doubling操作花费了4.2ms。Level和PCLHT的resizing代价更高,随着表大小而增长。Dash的代价略高于CCEH。

         CCEH的resizing代价最低,因为CCEH每次只需要分裂段,复制到新段中,旧的重复数据在更新时删掉(lazy deletion). Dash的结构与CCEH类似,区别在于Dash在每个bucket中维护32 bytes的metadata,移动到新段的items在旧段中会删除,这需要往PM中入更多数据。因此Dash中的段分裂更耗时(55.6𝜇s on average),而doubling代价与CCEH差不多。当插入117M items后花费4.2ms来扩展directory。

         尽管Level hashing的结构允许其在resizing时减少重哈希的次数,其代价仍比动态哈希表要高。Level hashing中的resizing可以分为3步:① 创建新表 ② 锁定整个表,把items移动到新表的底层 ③ 释放旧的底层表。底层表的bucket用于装顶层表中冲突的items。在resizing后,新的底层可能full而无法存储更多item,因此当新的顶层表中发生冲突时,又要resize。在resize过程中,Level需要删除原本的底层表以维持一致性,从而导致额外的PM写。Level的resize策略导致高成本。当插入172M items后需要58.8s来完成resizing操作。

         类似的,PCLHT也需要重哈希整个表,锁定整个哈希表然后创建一个翻倍容量的新表,再重新把所有items哈希到新表中,最后释放旧表空间,而不是像Level hashing那样删除旧表的所有items?。因此,PCLHT的代价比Level hashing低,但是仍旧挺大的,当插入105M items后需要31.1s来resize。在图9中比较resizing与整体执行时间。Level hashing花费了24.8%,PCLHT花费了13.6%,CCEH和Dash分别花费了5.0%和5.8%。由于Clevel使用后台线程进行resizing,与查询线程是并行的,因此其resizing的代价被隐藏了effectively hidden。

         Resize耗时从低到高:CCEH 略好于 Dash > PCLHT > Level

4.7 Load Factor

         填装因子是哈希表的关键指标:使用的slots/哈希表中slots总数。在图10中使用第4.1节中的insert workload来衡量load factor.

         CCEH最低,因为它探测距离短,容易触发段分裂。其默认探测距离是4 cachelines能容纳4个bucket或16个slots,即使段里的其他bucket中有空闲slots,仍有可能发生哈希冲突从而导致过早段分裂。因此CCEH的load factor只能达到默认探测距离的44%。当探测距离设置为64 (256 slots),填装因子可以增加到92%。然而过长的探测距离会降低其性能。

         Dash, Level, Clevel, PCLHT通过特定的优化得到更高的填装因子,最大值分别是86%, 79%, 83%, and 83%。Dash利用stashing, balanced insert, and displacement技术解决CCEH中过早段分裂的问题。Level/Clevel使每个key在resizing之前可以有2个位置可选,与cuckoo hashing类似。基于chaining的PCLHT比开放定址设计在调节冲突时更灵活。CCEH和Dash的填装因此没有像PCLHT和Level hashing一样下降剧烈(halved),因为在扩展表时只分裂1个段。SOFT的填装因子为100%,因此按需创建和删除,不维护空余空间。

         总之,除了SOFT之外,其他哈希表优化填装因子的方式都是探测更多备用的standby buckets,然而这与实现更高吞吐量的目标相悖。我们不能盲目的通过维护未使用的内存来提升性能。cannot blindly try to improve performance at the cost of maintaining a large amount of unutilized memory

         填装因子最高值大小从高到低:SOFT > Dash > Clevel ~ PCLHT > Level > CCEH

4.8 Memory Utilization

         哈希索引可能牺牲内存空间来实现特定的设计目标。例如,为了提高查询性能维护额外的metadata,为了优化cache access使得bucket cacheline-aligned (with unused bits)。这节评估哈希表的内存利用率,结果在表2中。利用率=键值对所占空间/哈希表实际分配空间

         SOFT的利用率大约22%,因为它在DRAM中维护了额外的数据拷贝。如果只考虑PM存储,SOFT接近100%,因为只有真实数据+3 validity bits存储在PM中。

         Level最大利用率大概52.6% 为了把bucket对齐到cacheline,每个bucket有13 unused bytes。Level原本的实现利用率更高,因为每个bucket(4 key-value pairs each consisting of a 16-byte key and a 15-byte value, and 1-byte token),对齐到2 cachelines。

         对于CCEH,唯一额外的空间消耗是directory中存储的指针,但是利用率低,大约44%因为线性探测设计导致load factor

         尽管Dash的每个bucket(256 bytes)有32B metadata,由于为了提高load factor进行特殊优化提高了内存利用率74.6%。PCLHT的利用率62.2%。PCLHT的每个bucket(64 bytes)包含8-byte的用于并发控制的word和next指针,以及3个键值对。Clevel需要存储指向键值对的指针,内存利用率大约55.3%. (利用率高低:Dash > PCLHT > Clevel > Level > CCEH > SOFT

4.9 Recovery

         当系统崩溃或掉电发生时,recovery对于表重建和不一致性清除是必要的。原本的PCLHT缺少恢复实现,因此按照静态哈希方法的恢复机制进行实现。

恢复时间的评估方法:① 加载一些键值对 ② 人工kill进程 ③ 重启进程,记录进程启动和准备好处理接下来请求两个状态之间的时间。 因此恢复时间包括2部分:启动进程时间(与系统有关,一般常数例如30ms),恢复哈希表结构的时间。

         表3中显示:Level, Clevel, PCLHT基本是常数的恢复时间,因为只需要检查是否有未完成的resizing操作。PCLHT只关心新表是否创建但没正确初始化,如果这样那新表被删掉,重新进行resizing。否则,进程可以serve requests immediately。Level检查指向新表顶层表的指针是否null,如果这样说明resizing未完成,与PCLHT类似的处理。Clevel除了检查重哈希的一致性,也会为了释放未使用内存而检查内存泄漏。‘

         CCEH的恢复时间随数据大小而变化,因为需要遍历directory来做不一致性检查。Dash也类似,但是代价被分摊amortized over segment accesses after recovering。因此,Dash的恢复时间比CCEH低。

         SOFT在DRAM中维护结构信息,除了完整遍历PM的数据,也需要重构DRAM中的结构,因此恢复时间是其他的3倍多。recovery time is 3 orders of magnitude larger than others      

恢复时间从少到多: PCLHT ~ Level ~ Clevel ~ Dash > CCEH > SOFT

4.10 Breakdown of Data Written to PM

         图3(a)中反映,当插入item时,写入PM的数据平均大小比实际键值对大小要高。为了理解这个现象,我们把在插入2M键值对(3.2GB)时,写入PM的数据total data划分成5部分:① Allocator 由于动态内存分配和回收造成的写数据 ② Resize 在resizing操作中写的数据 ③ K-V 真实键值对大小 ④ Lock 由于触发lock/unlock primitives造成的额外数据 ⑤ 其他数据。 使用PCM tools来直接衡量allocator和resize的大小。通过源码指令来收集lock的结果,因为使用PCM收集2M次锁很耗时,结果在图11中。

         Allocator. 当段分裂发生时,Dash和CCEH都需要为新段分配内存。Dash花费~8.5GB (18%), CCEH花费~3.9GB (13%)。这个差异的原因是他们使用不同的内存分配器。Dash依赖libpmemobj,需要为了崩溃安全产生logs,CCEH使用libvmem,不保证崩溃安全。

PCLHT和SOFT花费~4.4GB (11%) and ~3.5GB (28%),对于Level,data volume is ~41 MB,因为只有在resizing发生时才分配内存。Clevel使用PMDK的API make_persistent_object 来分配内存,与数据复制相关,因此无法直接衡量allocator产生的数据,所以分类到others。

         Resize. Level触发5次全表resizing,导致~6.8GB (16%)。PCLHT触发2次全表resizing,导致~4.6GB (11%) 。CCEH和Dash以段为扩充容量的粒度,因此比PCLHT触发更少的resizing data。CCEH更频繁的结构变更,因此resizing数据比例比Dash高。(i.e., 9% vs 5% or 3.0GB vs 2.2GB).

         Lock. 基于锁的哈希表CCEH, Dash和PCLHT,由于频繁触发锁和线程冲突,locking造成的额外数据很大,平均达到12.5GB (32%)。Level也要slot粒度的锁,为什么图里没有?)

         Others. 除了Clevel,其他哈希表log-free。Dash只在目录翻倍时使用日志,但是产生的数据可以忽略。对于CCEH, Dash, PCLHT, and SOFT,由于others造成的数据与由于写放大产生的数据差不多(accounting for 40% on average, 4x of that in K-V)。这与16-byte key-value pairs are flushed to PM at the granularity of 64 bytes的事实相符。

对于Level,others包括写放大与bucket之间移动item所产生的数据。对于Clevel,92%的数据都属于这一类,因为 ① 对每个插入的item都需要分配空间 ② 为了支持变长item要在哈希表中存储指针 ③ 每次内存分配和数据复制调用函数make_persistent_object都会产生日志条目

         以上分析反映,键值对只占写入PM数据的一小部分,其他的更大writes associated with memory allocation, synchronization primitives, and hash table specific features (like resizing) could be unexpectedly high。

         同样数量的KV,数据量从大到小:

        总的PM写数据:Clevel > Dash > PCLHT ~ Level > CCEH > SOFT

        动态分配回收:Dash > PCLHT > CCEH > SOFT > Level

        Resize操作:  Level ~ Clevel > PCLHT > CCEH > Dash

        Lock相关:   Dash > CCEH > PCLHT

        其他:  Clevel > Level > PCLHT > Dash > CCEH > SOFT (Clevel包括动态分配回收)

4.11 Issues Related to PM Hardware

         本节讨论与PM硬件相关的3个性能问题:1. when operating on the elements of hash tables, will there be contention for DIMMs? 在操作哈希表元素时是否导致DIMM冲突。有研究发现accesses across interleaved DIMMs may cause contention for particular DIMMs。本文在运行workload时使用PCM工具每15ms观察每个channel的带宽,发现总是稳定的,因此认为哈希表不会导致DIMM的冲突。   

         因为论文中评估的哈希表都没有达到DCPMM写带宽的上限,2个问题:how small random PM writes restrict the performance of hash tables。随机PM写会对哈希表的性能有何影响。本文设计了简化模型来探究为何哈希表的PM带宽利用率低。为了消除resizing, synchronization, and hash value computation的影响,我们使用1个大数组模拟没有这些操作的哈希表。使用32线程来随机插入16 bytes的items。另外评估了特定指令对整体性能的影响enabling only one or both of flush and fence instructions, or disabling both, or using non-temporal stores (NTstore)。

         图12中灰色柱状图反映吞吐量,正方形折线反映带宽。当插入16 bytes的items时,即使disable flush and fence指令,吞吐量也不超过31 Mops/s。因此可以认为低吞吐量和带宽利用率是由于small random write造成。但是目前的哈希表即使有cacheline alignment优化,都无法解决这个问题。可行的方法是把small random writes转化成large sequential ones。例如,把DRAM buffer中的random writes分组,然后顺序写入PM中。用简化模型对这个方法也进行评估,When items are packed into 64-byte buffers, under the same bandwidth, the throughput increases to 91Mops/s, and reaches 141Mops/s when we use 256-byte buffers。从图12中还可以发现对于large write,性能对flush和fence指令敏感,对于256-byte写,NTstore指令的吞吐量最高。 

         第3个问题:how much effect flush and fence instructions have on the performance of hash tables。指令对哈希表性能的影响。图13中反映,指令对所有哈希表的吞吐量影响小,与图12中16 bytes的情况相符。因为PM哈希表中的写操作造成的代价小 (using customized designs to ensure consistency) merely involve a few cacheline flushes and memory fences, as compared to the data structures that rely on software transactional memory random small write影响大,指令影响小)

4.12 Impact of NUMA

         本节讨论uniform下NUMA对吞吐量的影响。使用2个CPU,1个为local,1个为remote。所有DRAM和DCPMM安装在local CPU。启动32个线程pin them to CPU cores,共4种配置:① Local 所有线程在local CPU ② Half 16个在local,16个在remote ③ Remote 所有32个在remote ④ No-limit 由默认的OS scheduler进行线程管理。

         图14中反映访问remote node大部分情况下都会降低性能。插入操作中Remote的吞吐量比local低很多。Dash下降2.6x,CCEH 2.3x,Level和PCLHT 1.4x,影响稍微小一点。图14(b)中,通过比较Half和Remote的查询与插入,发现NUMA对读的影响比写小。原因是The ratio of read latency between local and remote DCPMM is 1.20x for random access. For write, the remote access latency of Optane DCPMM is 1.68x higher compared to local。哈希表呈现出exhibit fairly random access patterns。图14(c)中mixed workload与search趋势相似,因为删除操作多次读1次写。No-limit与Half接近甚至更好,因为OS可能优化线程管理。

         NUMA对性能有负面影响,但是running more threads on remote node can still be properly leveraged to improve performance, depending on the utilization of PM bandwidth。在第4.3节中,SOFT和PCLHT的吞吐量随着远程线程的增多而变大,CCEH和Dash can exhaust PM bandwidth with only local threads。NUMA有负面影响,不过也可以用于加速SOFT/PCLHT

5 DISCUSSIONS

1. Random small write is the primary factor that fundamentally restricts the performance of PM hash tables. 随机少量数据的写是哈希表中的inherent access pattern。即使有优化,例如减少随机写次数,现有的哈希表仍无法完全解决这个问题。因此需要新的哈希表设计,从而消除这种缺点,达到更高性能。

2. The impact of cache flush and memory fence instructions on performance is marginal. 与通常的使用软件transactional memory来对PM数据结构实验得到的结论不同,PM哈希表的性能与指令没有太大关系,因为指令数有限。优化应该放在设计哈希表结构上。

3. Fingerprinting can accelerate negative queries significantly. Cache-resident指纹可以加速负向搜索,因为可以过滤不必要的PM访问。由于非对称读写(写延迟更低),这对PM来说是有意义的做法。

4. Resizing should not discourage normal operations.全表重哈希不利于并发,会导致低吞吐量和不必要的延迟。动态扩展方法(Dash,CCEH)和lock-free resizing(Clevel)可以更好的减少resizing代价。Clevelresize是后台进程,真的减少了吗?)

5. Beware of the significance of write amplifications caused by memory allocators and synchronization primitives. 除了small random write造成的写放大,内存分配和同步原语也会造成写放大,可能比预想的还高。写放大会降低性能,损坏DCPMM的持久性endurance。未来计划设计wear-aware memory allocator for PM systems and PM-friendly locks

6. Concerns about hybrid memory designs. 在DRAM中保存结构metadata,在PM中保存键值对能减少PM读写和访问延迟。但是需要降低recovery cost

7. PM bandwidth is a scarce resource, but the performance of hash tables does not necessarily scale with more DCPMMs. 有些哈希表会随着DCPMM个数增多,性能也增大;有些不会。需要探究哈希表的可扩展性的原因。使用哈希表时要在DCPMM个数和DRAM资源之间进行平衡

8. Micro-architectural characteristics of PM matter. 哈希表设计者需要关注到XPBuffer的重要性,需要对这些性能敏感的微架构组件例如WPQ size, read-modify-write buffer, and address indirection translation buffer进行探索,从而优化PM数据结构的设计。

6 RELATED WORK

         基于DRAM的哈希表已经研究数十年,学者提出了单处理器和多处理器的哈希表。之前也有工作对5个基于DRAM的并发哈希表进行评估,使用统一框架和4个代表性多核平台。

         PM-based index structures. 除了第3章讨论的哈希表,最近也提出了一些新的哈希表。NVC-Hashmap是支持unordered dictionaries and delta indices for in-memory databases的lock-free哈希表。PFHT是使用stash bucket提高load factor的写优化哈希表。Path hashing为了避免更新操作的额外PM写,把buckets组织成inverted complete binary tree logically,由于查找依赖树高,这种结构造成了低查找性能。Level hashing提高基于共享的2层结构解决这个问题。基于PM的索引树也有很多研究,Lersch开发了PiBench,使用DCPMM对4种B+树进行了评估。

         Efforts to reduce consistency and durability overhead and programming difficulty.

 Transaction memory (TM)与PM交互,常被用于保证failure-atomicity and durability。但是,结合TM的PM索引的flushing和logging代价高。TIMESTONE使用混合日志技术TOC logging来保证事故一致性,且写放大低、内存footprint少。Minimally Ordered Durable (MOD)库提供类似STL的持久化数据结构接口,能减少开发PM应用的成本。Pronto使用asynchronous semantic logging来减少使用持久化存储的编程负担。

         PM indexes in data-intensive systems. 目前数据库系统、键值存储、文件系统中基础组件就是并发数据结构。然而这些系统的性能受workload大小限制,尤其是在系统在restart后需要恢复时。因此一些使用混合内存的存储系统被提出,HiKV是拥有混合索引的持久化键值存储,PM中哈希表,DRAM中B+树,继承了哈希表中的指针查询与B+树中的范围查找。基于PM的键值存储系统的问题是small-sized access pattern in key-value stores does not match with the persistence granularity in PM, leaving the PM bandwidth underutilized。FlatStore使用CCEH作为索引来解决上述问题。

7 CONCLUSION

         对基于DCPMM的几种哈希表进行了详细测评。扩展PiBench提供更多功能测评,不仅考虑常见指标,也考虑硬件相关的特性,例如PM bandwidth, flush/fence instructions, and NUMA architecture。指出实验得到的启发,有利于开发更高效、可扩展的基于PM的哈希表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anyanyamy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值