1 // Written in the D programming language.
2 
3 /**
4 Group and related families for Map
5 
6 Copyright: Copyright 2024- Masahiro Nakagawa
7 Authros:   Masahiro Nakagawa
8 License:   $(HTTP www.apache.org/licenses/LICENSE-2.0, Apache License Version 2.0)
9  */
10 module swisstable.group;
11 
12 import core.bitop : bsr, bsf, bswap;
13 
14 nothrow pure @safe @nogc
15 {
16 
17 alias H1 = ulong;
18 alias H2 = ubyte;
19 
20 /// Extract H1 portion from given hash. This value is used for slot index.
21 pragma(inline, true) H1 calcH1(size_t h) { return (h & 0xFFFF_FFFF_FFFF_FF80) >> 7; }
22 /// Extract H2 portion from given hash. This value is used for Control byte.
23 pragma(inline, true) H2 calcH2(size_t h) { return h & 0x7F; }
24 
25 enum Control : byte
26 {
27     Empty     = -128,
28     Tombstone = -2,  // kDeleted in original implementation
29     Sentinel  = -1
30 }
31 
32 // Helpers for Control state check
33 pragma(inline, true) bool isEmpty(Control c) { return c == Control.Empty; }
34 pragma(inline, true) bool isFull(Control c) { return c >= 0; }
35 pragma(inline, true) bool isTombstone(Control c) { return c == Control.Tombstone; }
36 pragma(inline, true) bool isEmptyOrTombstone(Control c) { return c < Control.Sentinel; }
37 
38 // TODO: Use ldc.intrinsics in LDC.
39 pragma(inline, true) uint countTrailingZeros(T)(T v)
40 {
41      return v ? cast(uint)bsf(v) : cast(uint)(8 * T.sizeof);
42 }
43 
44 unittest
45 {
46     assert(countTrailingZeros(0U) == 32);
47     assert(countTrailingZeros(0UL) == 64);
48     assert(countTrailingZeros(0b0010) == 1);
49     assert(countTrailingZeros(0b01000000) == 6);
50 }
51 
52 pragma(inline, true) uint countLeadingZeros(T)(T v)
53 {
54     static if (is(T == ubyte)) {
55         return v ? 7 - bsr(v) : 8;
56     } else static if (is(T == ushort)) {
57         return v ? 15 - bsr(v) : 16;
58     } else static if (is(T == uint)) {
59         return v ? 31 - bsr(v) : 32;
60     } else {
61         return v ? 63 - bsr(v) : 64;
62     }
63 }
64 
65 unittest
66 {
67     assert(countLeadingZeros(0U) == 32);
68     assert(countLeadingZeros(0UL) == 64);
69     assert(countLeadingZeros(cast(ubyte)0b01000000) == 1);
70     assert(countLeadingZeros(0x0080_0000U) == 8);
71 }
72 
73 } // nothrow pure @safe @nogc
74 
75 Control* emptyGroup() nothrow @trusted
76 {
77     static Control zero() { return cast(Control)0; }
78     static immutable Control[32] EmptyGroup = [
79         zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero, zero,
80         Control.Sentinel, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty,
81         Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty, Control.Empty
82     ];
83 
84     return cast(Control*)EmptyGroup.ptr + 16;
85 }
86 
87 private enum ulong LSBs = 0x0101010101010101UL;
88 private enum ulong MSBs = 0x8080808080808080UL;
89 
90 /**
91  * Iterable Mask for bits operation. This object is used in Map's lookup operation with Group.
92  *
93  * Bits and Shift are depend on Group implementation(SSE2 / Porable).
94  */
95 struct BitMask(T, int Bits, int Shift = 0)
96 {
97   private:
98     T _mask;
99 
100   public nothrow @safe:
101     this(T mask) { _mask = mask; }
102     bool opCast(T : bool)() const { return _mask != 0; }
103     bool empty() const { return _mask == 0; }
104     uint front() const { return (countTrailingZeros(_mask) >> Shift); }
105     void popFront() { _mask &= (_mask - 1); }
106     uint highestBitSet() const { return cast(uint)(bsr(_mask) >> Shift); }
107     uint leadingZeros() const
108     {
109         enum int extraBits = cast(int)((T.sizeof * 8) - (Bits << Shift));
110         return cast(uint)(countLeadingZeros(cast(T)(_mask << extraBits))) >> Shift;
111     }
112     alias lowestBitSet = front;
113     alias trailingZeros = front;
114 }
115 
116 unittest
117 {
118     import std.algorithm : equal;
119 
120     alias BitMaskUbyte = BitMask!(ubyte, 8);
121     assert(equal(BitMaskUbyte(0x1), [0]));
122     assert(equal(BitMaskUbyte(0x5), [0, 2]));
123     assert(equal(BitMaskUbyte(0x55), [0, 2, 4, 6]));
124     assert(equal(BitMaskUbyte(0xaa), [1, 3, 5, 7]));
125 
126     alias BitMaskUlong = BitMask!(ulong, 8, 3);
127     assert(equal(BitMaskUlong(0x0000000080800000UL), [2, 3]));
128     assert(equal(BitMaskUlong(MSBs), [0, 1, 2, 3, 4, 5, 6, 7]));
129     assert(equal(BitMaskUlong(0x8000808080008000UL), [1, 3, 4, 5, 7]));
130 }
131 
132 version (D_SIMD)
133 {
134 
135 /**
136  * Process a group of control bytes using SIMD/SSE2
137  */
138 struct GroupSSE2
139 {
140     import core.simd;
141 
142     enum size_t Width = 16;
143     alias BitMaskType = BitMask!(ushort, Width);
144 
145   private:
146     byte16 ctrl;
147 
148   public nothrow @trusted:
149     this(in Control* c)
150     {
151         ctrl = loadUnaligned!(byte16)(cast(const byte16*)c);
152     }
153 
154     /// Returns: a BitMask representing the positions of `hash` matched slots
155     pragma(inline, true) BitMaskType match(in H2 hash) const
156     {
157         auto m = mm_set1_epi8(hash);
158         return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPEQB, m, ctrl)));
159     }
160 
161     /// Returns: a BitMask representing the positions of empty slots
162     pragma(inline, true) BitMaskType maskEmpty() const
163     {
164         return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PSIGNB, ctrl, ctrl)));
165     }
166 
167     /// Returns: a BitMask representing the positions of full slots
168     BitMaskType maskFull() const
169     {
170         return BitMaskType(cast(ushort)mm_movemask_epi8(ctrl) ^ 0xffff);
171     }
172 
173     /// Returns: a BitMask representing the positions of non-full slots
174     BitMaskType maskNonFull() const
175     {
176         return BitMaskType(cast(ushort)mm_movemask_epi8(ctrl));
177     }
178 
179     /// Returns: a BitMask representing the positions of empty or tombstone slots
180     BitMaskType maskEmptyOrTombstone() const
181     {
182         auto m = mm_set1_epi8(Control.Sentinel);
183         return BitMaskType(cast(ushort)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPGTB, m, ctrl)));
184     }
185 
186     /// Returns: the number of trailing empty or tombstone slots in the group.
187     uint countLeadingEmptyOrTombstone() const
188     {
189         auto m = mm_set1_epi8(Control.Sentinel);
190         return countTrailingZeros(cast(uint)mm_movemask_epi8(cast(byte16)__simd(XMM.PCMPGTB, m, ctrl)) + 1);
191     }
192 
193     /// Convert empty/tombstone to empty and full to tombstone in the group..
194     void convertSpecialToEmptyAndFullToTombstone(Control* dst) const
195     {
196         const msbs = mm_set1_epi8(-128);
197         const x126 = mm_set1_epi8(126);
198         auto res = cast(byte16)__simd(XMM.POR, __simd(XMM.PSHUFB, x126, ctrl), msbs);
199         storeUnaligned!(byte16)(cast(byte16*)dst, res);
200     }
201 
202   private:
203     pragma(inline, true) byte16 mm_set1_epi8(in byte v) pure const
204     {
205         return byte16(v);
206     }
207 
208     // core.simd doesn't support XMM.PMOVMSKB
209     pragma(inline, true) int mm_movemask_epi8(in byte16 v) pure const
210     {
211         asm nothrow pure
212         {
213             naked;
214             pmovmskb EAX, XMM0;
215             ret;
216         }
217     }
218 }
219 
220 unittest
221 {
222     import std.algorithm : equal;
223 
224     Control[16] group = cast(Control[16])[
225         Control.Empty,  1, Control.Tombstone, 3, Control.Empty, 5, Control.Sentinel, 7,
226         7, 5, 3, 1, 1, 1, 1, 1];
227 
228     auto g = GroupSSE2(group.ptr);
229     // match
230     assert(g.match(0).empty);
231     assert(equal(g.match(1), [1, 11, 12, 13, 14, 15]));
232     assert(equal(g.match(3), [3, 10]));
233     assert(equal(g.match(5), [5, 9]));
234     assert(equal(g.match(7), [7, 8]));
235     // maskEmpty
236     assert(g.maskEmpty.lowestBitSet == 0);
237     assert(g.maskEmpty.highestBitSet == 4);
238     // maskEmptyOrTombstone
239     assert(g.maskEmptyOrTombstone.lowestBitSet == 0);
240     assert(g.maskEmptyOrTombstone.highestBitSet == 4);
241 }
242 
243 unittest
244 {
245     // GroupSSE2 maskFull / maskNonFull
246     import std.algorithm : equal;
247 
248     Control[16] group = cast(Control[16])[
249         Control.Empty,  1, Control.Tombstone, 3, Control.Empty, 5, Control.Sentinel, 7,
250         7, 5, Control.Tombstone, 1, 1, Control.Sentinel, Control.Empty, 1];
251 
252     auto g = GroupSSE2(group.ptr);
253     assert(equal(g.maskFull, [1, 3, 5, 7, 8, 9, 11, 12, 15]));
254     assert(equal(g.maskNonFull, [0, 2, 4, 6, 10, 13, 14]));
255 }
256 
257 } // D_SIMD
258 
259 /**
260  * Process a group of control bytes using portable bit operation
261  */
262 struct GroupPortable
263 {
264     enum size_t Width = 8;
265 
266     alias BitMaskType = BitMask!(ulong, Width, 3);
267 
268   private:
269     ulong ctrl;
270 
271   public nothrow @safe:
272     this(in Control* ctrl)
273     {
274         this.ctrl = load64bits(ctrl);
275     }
276 
277     pragma(inline, true) BitMaskType match(H2 hash) const
278     {
279         auto x = ctrl ^ (LSBs * hash);
280         return BitMaskType((x - LSBs) & ~x & MSBs);
281     }
282 
283     pragma(inline, true) BitMaskType maskEmpty() const
284     {
285         return BitMaskType((ctrl & ~(ctrl << 6)) & MSBs);
286     }
287 
288     BitMaskType maskFull() const
289     {
290         return BitMaskType((ctrl ^ MSBs) & MSBs);
291     }
292 
293     BitMaskType maskNonFull() const
294     {
295         return BitMaskType(ctrl & MSBs);
296     }
297 
298     BitMaskType maskEmptyOrTombstone() const
299     {
300         return BitMaskType((ctrl & ~(ctrl << 7)) & MSBs);
301     }
302 
303     uint countLeadingEmptyOrTombstone() const
304     {
305         return cast(uint)(countTrailingZeros((ctrl | ~(ctrl >> 7)) & LSBs) >> 3);
306     }
307 
308     void convertSpecialToEmptyAndFullToTombstone(Control* dst) const
309     {
310         auto x = ctrl & MSBs;
311         store64bits(dst, (~x + (x >> 7)) & ~LSBs);
312     }
313 }
314 
315 version (D_SIMD)
316     alias Group = GroupSSE2;
317 else
318     alias Group = GroupPortable;
319 
320 unittest
321 {
322     import std.algorithm : equal;
323 
324     Control[8] group = cast(Control[8])[Control.Empty,  1, 2, Control.Tombstone, 2, 1, Control.Sentinel, 1];
325 
326     // match
327     assert(GroupPortable(group.ptr).match(0).empty);
328     assert(equal(GroupPortable(group.ptr).match(1), [1, 5, 7]));
329     assert(equal(GroupPortable(group.ptr).match(2), [2, 4]));
330 
331     // maskEmpty
332     assert(GroupPortable(group.ptr).maskEmpty().lowestBitSet == 0);
333     assert(GroupPortable(group.ptr).maskEmpty().highestBitSet == 0);
334 
335     // maskEmptyOrTombstone
336     assert(GroupPortable(group.ptr).maskEmptyOrTombstone().lowestBitSet == 0);
337     assert(GroupPortable(group.ptr).maskEmptyOrTombstone().highestBitSet == 3);
338 
339     Control[8] group2 = cast(Control[8])[Control.Empty,  1, Control.Empty, Control.Tombstone, 2, Control.Sentinel, Control.Sentinel, 1];
340 
341     // maskFull / maskNonFull
342     assert(equal(GroupPortable(group2.ptr).maskFull(), [1, 4, 7]));
343     assert(equal(GroupPortable(group2.ptr).maskNonFull(), [0, 2, 3, 5, 6]));
344 }
345 
346 unittest
347 {
348     import std.meta;
349 
350     version (D_SIMD)
351         alias TestTypes = AliasSeq!(GroupPortable, GroupSSE2);
352     else
353         alias TestTypes = AliasSeq!(GroupPortable);
354 
355     // Group's convertSpecialToEmptyAndFullToTombstone test for dropDeletesWithoutResize()
356     foreach (TestGroup; TestTypes) {
357         enum size_t Cap = 63;
358         enum size_t GW = TestGroup.Width;
359 
360         Control[] ctrl = new Control[](Cap + 1 + GW);
361         ctrl[Cap] = Control.Sentinel;
362         Control[] pattern = cast(Control[])[Control.Empty, 2, Control.Tombstone, 2, Control.Empty, 1, Control.Tombstone];
363         for (size_t i; i != Cap; i++) {
364             ctrl[i] = pattern[i % pattern.length];
365             if (i < GW - 1)
366                 ctrl[i + Cap + 1] = pattern[i % pattern.length];
367         }
368         Control* ctrlPtr = ctrl.ptr;
369         for (Control* pos = ctrlPtr; pos < ctrlPtr + Cap; pos += GW)
370             TestGroup(pos).convertSpecialToEmptyAndFullToTombstone(pos);
371         memcpy(ctrlPtr + Cap + 1, ctrlPtr, GW - 1);
372         ctrl[Cap] = Control.Sentinel;
373 
374         for (size_t i; i < Cap + GW; i++) {
375             Control expected = pattern[i % (Cap + 1) % pattern.length];
376             if (i == Cap)
377                 expected = Control.Sentinel;
378             if (expected == Control.Tombstone)
379                 expected = Control.Empty;
380             if (expected.isFull)
381                 expected = Control.Tombstone;
382             assert(ctrl[i] == expected);
383         }
384     }
385 }
386 
387 unittest
388 {
389     import std.meta :  AliasSeq;
390     import std.array : array;
391     import std.range : repeat;
392 
393     version (D_SIMD)
394         alias TestTypes = AliasSeq!(GroupPortable, GroupSSE2);
395     else
396         alias TestTypes = AliasSeq!(GroupPortable);
397 
398     // Group's countLeadingEmptyOrTombstone test
399     foreach (TestGroup; TestTypes) {
400         Control[] empties = [Control.Empty, Control.Tombstone];
401         Control[] fulls = cast(Control[])[0, 1, 2, 3, 5, 9, 126, Control.Sentinel];
402 
403         foreach (empty; empties) {
404             Control[] e = empty.repeat(TestGroup.Width).array;
405             assert(TestGroup.Width == TestGroup(e.ptr).countLeadingEmptyOrTombstone());
406 
407             foreach (full; fulls) {
408                 foreach (i; 0..TestGroup.Width) {
409                     Control[] f = empty.repeat(TestGroup.Width).array;
410                     f[i] = full;
411                     assert(i == TestGroup(f.ptr).countLeadingEmptyOrTombstone);
412                 }
413                 Control[] f = empty.repeat(TestGroup.Width).array;
414                 f[TestGroup.Width * 2 / 3] = full;
415                 f[TestGroup.Width / 2] = full;
416                 assert((TestGroup.Width / 2) == TestGroup(f.ptr).countLeadingEmptyOrTombstone);
417             }
418         }
419     }
420 }
421 
422 import core.stdc.string : memcpy;
423 
424 nothrow @trusted @nogc:
425 
426 ulong load64bits(in Control* src)
427 {
428     ulong r;
429     memcpy(&r, src, ulong.sizeof);
430 
431     version (LittleEndian)
432     {
433         return r;
434     }
435     else
436     {
437         return bswap(r);        
438     }
439 }
440 
441 void store64bits(Control* dst, ulong src)
442 {
443     version (LittleEndian)
444     {
445         memcpy(dst, &src, ulong.sizeof);
446     }
447     else
448     {
449         ulong v = bswap(src);
450         memcpy(dst, &v, ulong.sizeof);
451     }
452 }