Setting the file. One moment.
Wasm Int128 Experiment · Clickhouse Js Node Rowbinary · ClickHouse/agent-skills · Skills Docs
ContentsBack to the top of the page Bundled file Tsconfig Build
tests/ wasm-int128.experiment.mjs
JavaScript · 246 lines · 7 KB
15 */
16 const URL_BASE = process.env. CLICKHOUSE_URL ?? "http://localhost:8123" ;
17 const N = 2_000_000 ;
18
19 // --- 1. hand-emit a tiny WASM module: void sum128(i32 ptr, i32 lenBytes) ----
20 // It folds 16-byte little-endian Int128s into a 128-bit accumulator (two i64s
21 // with carry) and writes [lo @ mem0, hi @ mem8]. The whole point: the per-row
22 // add never touches the JS heap.
23 const leb = ( n ) => {
24 const out = [];
25 do {
26 let b = n & 0x7f ;
27 n >>>= 7 ;
28 if (n) b |= 0x80 ;
29 out. push (b);
30 } while (n);
31 return out;
32 };
33 const str = ( s ) => [s. length , ... [ ... Buffer. from (s)]];
34 const section = ( id , content ) => [id, ... leb (content. length ), ... content];
35
36 // locals after params (0=ptr i32, 1=len i32):
37 // 2=p i32, 3=end i32, 4=accLo i64, 5=accHi i64, 6=lo i64, 7=hi i64, 8=newLo i64
38 const body = [
39 0x02 ,
40 0x02 ,
41 0x7f ,
42 0x05 ,
43 0x7e , // locals: 2×i32, 5×i64
44 0x20 ,
45 0x00 ,
46 0x21 ,
47 0x02 , // p = ptr
48 0x20 ,
49 0x00 ,
50 0x20 ,
51 0x01 ,
52 0x6a ,
53 0x21 ,
54 0x03 , // end = ptr + len
55 0x02 ,
56 0x40 , // block
57 0x03 ,
58 0x40 , // loop
59 0x20 ,
60 0x02 ,
61 0x20 ,
62 0x03 ,
63 0x4f ,
64 0x0d ,
65 0x01 , // if p >=u end: break
66 0x20 ,
67 0x02 ,
68 0x29 ,
69 0x03 ,
70 0x00 ,
71 0x21 ,
72 0x06 , // lo = i64.load[p]
73 0x20 ,
74 0x02 ,
75 0x29 ,
76 0x03 ,
77 0x08 ,
78 0x21 ,
79 0x07 , // hi = i64.load[p+8]
80 0x20 ,
81 0x04 ,
82 0x20 ,
83 0x06 ,
84 0x7c ,
85 0x21 ,
86 0x08 , // newLo = accLo + lo
87 0x20 ,
88 0x05 ,
89 0x20 ,
90 0x07 ,
91 0x7c , // accHi + hi
92 0x20 ,
93 0x08 ,
94 0x20 ,
95 0x04 ,
96 0x54 ,
97 0xad ,
98 0x7c , // + (newLo <u accLo ? 1 : 0) carry
99 0x21 ,
100 0x05 , // accHi = ...
101 0x20 ,
102 0x08 ,
103 0x21 ,
104 0x04 , // accLo = newLo
105 0x20 ,
106 0x02 ,
107 0x41 ,
108 0x10 ,
109 0x6a ,
110 0x21 ,
111 0x02 , // p += 16
112 0x0c ,
113 0x00 , // continue loop
114 0x0b , // end loop
115 0x0b , // end block
116 0x41 ,
117 0x00 ,
118 0x20 ,
119 0x04 ,
120 0x37 ,
121 0x03 ,
122 0x00 , // mem[0] = accLo
123 0x41 ,
124 0x00 ,
125 0x20 ,
126 0x05 ,
127 0x37 ,
128 0x03 ,
129 0x08 , // mem[8] = accHi
130 0x0b , // end function
131 ];
132
133 const moduleBytes = Uint8Array. from ([
134 0x00 ,
135 0x61 ,
136 0x73 ,
137 0x6d ,
138 0x01 ,
139 0x00 ,
140 0x00 ,
141 0x00 , // magic + version
142 ... section ( 1 , [ 0x01 , 0x60 , 0x02 , 0x7f , 0x7f , 0x00 ]), // type: (i32,i32)->()
143 ... section ( 3 , [ 0x01 , 0x00 ]), // func 0 : type 0
144 ... section ( 5 , [ 0x01 , 0x00 , 0x02 ]), // memory: min 2 pages
145 ... section ( 7 , [
146 0x02 ,
147 ... str ( "memory" ),
148 0x02 ,
149 0x00 ,
150 ... str ( "sum128" ),
151 0x00 ,
152 0x00 ,
153 ]),
154 ... section ( 10 , [ 0x01 , ... leb (body. length ), ... body]), // code
155 ]);
156
157 const mod = await WebAssembly. compile (moduleBytes); // throws if malformed — a free validator
158 const inst = await WebAssembly. instantiate (mod, {});
159 const { memory , sum128 } = inst.exports;
160
161 // --- fetch the Int128 column ------------------------------------------------
162 const sql = `SELECT toInt128(number) * 123456789012345 AS x FROM numbers(${ N }) FORMAT RowBinary` ;
163 const res = await fetch ( URL_BASE , { method: "POST" , body: sql });
164 if ( ! res.ok) throw new Error ( `ClickHouse ${ res . status }: ${ await res . text () }` );
165 const buf = Buffer. from ( await res. arrayBuffer ());
166 const view = new DataView (buf.buffer, buf.byteOffset, buf.byteLength);
167 const MB = buf. length / 1e6 ;
168 const reps = 20 ;
169 const ms = ( t ) => Number (t) / 1e6 / reps;
170 const gbs = ( msPass ) => MB / 1e3 / (msPass / 1e3 );
171
172 // --- 1. JS BigInt 128-bit sum (correct; what you must do today) -------------
173 let acc = 0 n ;
174 let t = process.hrtime. bigint ();
175 for ( let r = 0 ; r < reps; r ++ ) {
176 acc = 0 n ;
177 for ( let o = 0 ; o < buf. length ; o += 16 ) {
178 const lo = view. getBigUint64 (o, true );
179 const hi = view. getBigInt64 (o + 8 , true );
180 acc += (hi << 64 n ) + lo;
181 }
182 }
183 const bigintMs = ms (process.hrtime. bigint () - t);
184
185 // --- 2. JS f64 fold: the raw read floor (V8 at memory speed) ----------------
186 let sink = 0 ;
187 t = process.hrtime. bigint ();
188 for ( let r = 0 ; r < reps; r ++ ) {
189 let a = 0 ;
190 for ( let o = 0 ; o < buf. length ; o += 16 )
191 a += view. getFloat64 (o, true ) + view. getFloat64 (o + 8 , true );
192 sink = a;
193 }
194 const floorMs = ms (process.hrtime. bigint () - t);
195
196 // --- 3. WASM kernel ---------------------------------------------------------
197 const INPUT_OFF = 64 ; // results live in mem[0..16); input clear of them
198 const needPages = Math. ceil (( INPUT_OFF + buf. length ) / 65536 );
199 const havePages = memory.buffer.byteLength / 65536 ;
200 if (needPages > havePages) memory. grow (needPages - havePages);
201
202 // boundary tax: copy the network buffer into linear memory
203 t = process.hrtime. bigint ();
204 for ( let r = 0 ; r < reps; r ++ )
205 new Uint8Array (memory.buffer, INPUT_OFF , buf. length ). set (buf);
206 const copyMs = ms (process.hrtime. bigint () - t);
207
208 // kernel only (buffer already resident)
209 t = process.hrtime. bigint ();
210 for ( let r = 0 ; r < reps; r ++ ) sum128 ( INPUT_OFF , buf. length );
211 const kernelMs = ms (process.hrtime. bigint () - t);
212
213 const mview = new DataView (memory.buffer);
214 const wasmSum =
215 (mview. getBigUint64 ( 8 , true ) << 64 n ) + mview. getBigUint64 ( 0 , true );
216 if (wasmSum !== acc) throw new Error ( `WASM sum ${ wasmSum } != BigInt ${ acc }` );
217
218 console. log (
219 ` \n Int128 column sum — ${ N . toLocaleString () } rows, ${ MB . toFixed ( 0 ) } MB, ${ reps } reps (Node ${ process . version })` ,
220 );
221 console. log ( ` correctness: WASM == BigInt == ${ acc } ✓ \n ` );
222 const row = ( name , msPass , note = "" ) =>
223 console. log (
224 ` ${ name . padEnd ( 34 ) } ${ msPass . toFixed ( 2 ). padStart ( 7 ) } ms ${ gbs ( msPass ). toFixed ( 1 ). padStart ( 5 ) } GB/s ${ note }` ,
225 );
226 row ( "1. JS BigInt-128 sum (correct)" , bigintMs, "← what JS must do" );
227 row ( "2. JS f64 fold (read floor)" , floorMs, "← V8 at memory speed" );
228 row ( "3. WASM i64 add-carry, kernel only" , kernelMs);
229 row (
230 " WASM + copy-in boundary tax" ,
231 kernelMs + copyMs,
232 `(copy ${ copyMs . toFixed ( 2 ) } ms)` ,
233 );
234 console. log (
235 ` \n BigInt tax vs read floor : ${ ( bigintMs / floorMs ). toFixed ( 1 ) }x` ,
236 );
237 console. log (
238 ` WASM kernel vs BigInt : ${ ( bigintMs / kernelMs ). toFixed ( 1 ) }x faster` ,
239 );
240 console. log (
241 ` WASM+copy vs BigInt : ${ ( bigintMs / ( kernelMs + copyMs )). toFixed ( 1 ) }x faster` ,
242 );
243 console. log (
244 ` WASM kernel vs read floor: ${ ( kernelMs / floorMs ). toFixed ( 2 ) }x (1.0 = at memory speed)` ,
245 );
246 if (sink === undefined ) throw new Error ( "floor sink" ); // keep `sink` observable so V8 can't elide the fold