Skip to content

Commit cf176f4

Browse files
committed
branchless/batchy parse digits
1 parent e94a028 commit cf176f4

File tree

1 file changed

+174
-24
lines changed

1 file changed

+174
-24
lines changed

vertx-sql-client/src/main/java/io/vertx/sqlclient/impl/codec/CommonCodec.java

Lines changed: 174 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,185 @@
1212
package io.vertx.sqlclient.impl.codec;
1313

1414
import io.netty.buffer.ByteBuf;
15+
import io.netty.buffer.Unpooled;
1516

17+
/**
18+
* This is based on this algorithm: https://lemire.me/blog/2022/01/21/swar-explained-parsing-eight-digits/
19+
* Which can be explained as follows:
20+
* <pre>
21+
*
22+
* Given 8 ASCII digits as: b1b2b3b4b5b6b7b8
23+
*
24+
* eg: "12345678"
25+
*
26+
* which byte[] := { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38 }
27+
* "1" "2" "3" "4" "5" "6" "7" "8"
28+
*
29+
* reading is in Little Endian form will read the lower indexes first placing them as lower addresses
30+
* as can be seen in the hex representation, where "1" is now in the rightmost position.
31+
*
32+
* hex:
33+
* 0x38_37_36_35_34_33_32_31-
34+
* 0x30_30_30_30_30_30_30_30=
35+
* 0x80_07_06_05_04_03_02_01
36+
*
37+
* digits = (digits * 10) + (digits >> 8);
38+
*
39+
* 0x50_46_3c_32_28_1e_14_0a + ~ digits * 10 +
40+
* 0x00_80_70_60_50_40_30_21 = ~ digits >> 8 =
41+
* 0x50_4e_43_38_2d_22_17_0c
42+
* now ^ ^ ^ ^
43+
* | | | |
44+
* g4=10*b7+b8 | | |
45+
* g3=10*b5+b6 | |
46+
* g2=10*b3+b4 |
47+
* g1=10*b1+b2
48+
*
49+
* These are the relevant results we care about, while the others are useless
50+
* and subsequent masks will take care to ignore them.
51+
*
52+
* Now the aggregation parts:
53+
*
54+
* digits & U64_MASK := 0x00_00_00_38_00_00_00_0c
55+
*
56+
* This will isolate 10*b3+b4 and 10*b7+b8, trying to correctly compute:
57+
*
58+
* 1000000*(10*b1+b2) + 100*(10*b5+b6), somehow.
59+
*
60+
* The mask used to perform the multiplication is
61+
*
62+
* U64_FIRST_THIRD := (1000000L << 32) + 100 := 0x00_0f_42_40_00_00_00_64
63+
*
64+
* 0x00_00_00_38_00_00_00_0c *
65+
* 0x00_0f_42_40_00_00_00_64 =
66+
* 0x00_b7_30_e0_00_00_04_b0
67+
*
68+
*
69+
* which 0x00_b7_30_e0 part (let's ignore the second half 00_00_04_b0, which is 1200)
70+
*
71+
* is, in decimal:
72+
*
73+
* 12005600 (!!!) === 1000000*(10*1 + 2) + 100*(10*5 + 6) = 1*10000000 + 2*1000000 + 5*1000 + 6*100
74+
*
75+
* For the second part
76+
*
77+
* ie 10000*(10*b3 + b4) + 10*b7 + b8
78+
*
79+
* we first isolate the 2 paris (g2 and g4) with (digits >> 16) & U64_MASK (which move them to the right by 2 bytes), getting
80+
*
81+
* 0x00_00_4e_00_00_00_22 *
82+
* 0x00_27_10_00_00_00_01 =
83+
* 0x05_30_6e_00_00_00_22
84+
*
85+
* which, once again, has it leftmost part
86+
*
87+
* 0x05_30_6e === 340078 (!!!!) === 10000*(10*3 + 4) + 10*7 + 8 = 3*100000 + 4*10000 + 7*10 + 8
88+
*
89+
*
90+
* shifting both left by 32 and adding them, the total digit is done.
91+
* </pre>
92+
*/
1693
public class CommonCodec {
17-
/**
18-
* Decode the specified {@code buff} formatted as a decimal string starting at the readable index
19-
* with the specified {@code length} to a long.
20-
*
21-
* @param index the hex string index
22-
* @param len the hex string length
23-
* @param buff the byte buff to read from
24-
* @return the decoded value as a long
25-
*/
94+
95+
// https://lemire.me/blog/2022/01/21/swar-explained-parsing-eight-digits/
96+
private static final long U64_MASK = 0x000000FF000000FFL;
97+
private static final long U64_FIRST_THIRD = (1000000L << 32) + 100;
98+
private static final long U64_SECOND_FOURTH = (10000L << 32) + 1;
99+
100+
private static long parseEigthDigitsLE(long digits) {
101+
digits -= 0x3030303030303030L;
102+
digits = (digits * 10) + (digits >> 8);
103+
return (((digits & U64_MASK) * U64_FIRST_THIRD) + (((digits >> 16) & U64_MASK) * U64_SECOND_FOURTH)) >> 32;
104+
}
105+
106+
private static final int U32_MASK = 0x00FF00FF;
107+
private static final int U32_FIRST_SECOND = (100 << 16) + 1;
108+
109+
private static int parseFourDigitsLE(int digits) {
110+
digits -= 0x30303030;
111+
digits = (digits * 10) + (digits >> 8);
112+
return ((digits & U32_MASK) * U32_FIRST_SECOND) >> 16;
113+
}
114+
115+
private static short parseTwoDigitsLE(short digits) {
116+
digits -= 0x3030;
117+
return (short) ((digits & 0xFF) * 10 + ((digits >> 8) & 0xFF));
118+
}
119+
120+
private static byte parseOneDigit(byte digit) {
121+
return (byte) (digit - 0x30);
122+
}
123+
124+
public static void main(String[] args) {
125+
ByteBuf buff = Unpooled.buffer();
126+
buff.writeCharSequence("-123", java.nio.charset.StandardCharsets.UTF_8);
127+
System.out.println(decodeDecStringToLong(0, buff.readableBytes(), buff));
128+
}
129+
130+
public static int decodeDecStringToInt(int index, int len, ByteBuf buff) {
131+
// 10 + sign = 32bit
132+
return 0;
133+
}
134+
135+
public static int decodeDecStringToShort(int index, int len, ByteBuf buff) {
136+
// 5 + sign = 16bit
137+
return 0;
138+
}
139+
26140
public static long decodeDecStringToLong(int index, int len, ByteBuf buff) {
27-
long value = 0;
28-
if (len > 0) {
29-
int to = index + len;
30-
boolean neg = false;
31-
if (buff.getByte(index) == '-') {
32-
neg = true;
33-
index++;
34-
}
35-
while (index < to) {
36-
byte ch = buff.getByte(index++);
37-
byte nibble = (byte) (ch - '0');
38-
value = value * 10 + nibble;
141+
byte firstByte = buff.getByte(index);
142+
final boolean negative = firstByte == '-';
143+
// handling these fast-path to avoid using get<something>LE which is not free
144+
if (len <= 2) {
145+
if (len == 1) {
146+
if (negative) {
147+
throw new IllegalArgumentException("Invalid negative number: missing digits");
148+
}
149+
return parseOneDigit(firstByte);
39150
}
40-
if (neg) {
41-
value = -value;
151+
assert len == 2;
152+
if (negative) {
153+
return -parseOneDigit(buff.getByte(index + 1));
42154
}
155+
return parseOneDigit(firstByte) * 10 + parseOneDigit(buff.getByte(index + 1));
156+
}
157+
if (negative) {
158+
index++;
159+
len--;
160+
}
161+
long lessThanEight = len % 8;
162+
if (lessThanEight > 0) {
163+
return lessThanEightDigitsUnrolled(negative, index, len, buff);
164+
}
165+
throw new UnsupportedOperationException("Not implemented yet");
166+
}
167+
168+
private static int lessThanEightDigitsUnrolled(boolean negative, int index, int len, ByteBuf buff) {
169+
assert len > 0 && len < 8;
170+
int digits = 0;
171+
int multiplier = 1;
172+
// len >= 4
173+
if ((len & Integer.BYTES) != 0) {
174+
digits = parseFourDigitsLE(buff.getIntLE(index));
175+
index += Integer.BYTES;
176+
multiplier = 100;
177+
}
178+
// len >= 2
179+
if ((len & Short.BYTES) != 0) {
180+
digits = digits * multiplier + parseTwoDigitsLE(buff.getShortLE(index));
181+
index += Short.BYTES;
182+
multiplier = 10;
183+
}
184+
// len >= 1
185+
if ((len & Byte.BYTES) != 0) {
186+
digits = digits * multiplier + parseOneDigit(buff.getByte(index));
43187
}
44-
return value;
188+
return negative ? -digits : digits;
45189
}
190+
191+
public static byte decodeDecStringToByte(int index, int len, ByteBuf buff) {
192+
// 3 + sign = 8bit
193+
return 0;
194+
}
195+
46196
}

0 commit comments

Comments
 (0)