Sign in Join
Wiki copytemporary

Case Compiler Optimization: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## x86
22
33### Jump Table
44
55- Complexity O(1).
66
77#### Requirements
88
99- 50 or more branches.
1010- Case labels are not sparse.
1111
1212#### Overview
1313
1414When the number of individual branches grows to a large value, it becomes computationally expensive to test each valid value one-by-one; this is especially the case if a common branch appears at the end of the list (e.g. the largest value of a given set). A jump table seeks to eliminate this penalty by mapping each branch's address into a direct lookup table. Any gaps in the table can easily be filled with an address to the **else** block (or the end of the case block if one isn't present).
1515
1616Nevertheless, the lookup table is stored separately to the current code block and must be fetched from memory; this, on top of a near jump, consumes a large number of cycles where the CPU is waiting for the address to reach a register, sometimes to the point where it's faster to simply compare the input contained within a register with each case label individually, since the current block of code is already in the cache and all work is done through immediate values. This is why such jump tables are only beneficial with a large number of branches.
1717
1818This system, however, breaks down if the range of values is sparse, since this produces an excessively large lookup table where most addresses point to the **else** block. Even if storage space is not a concern, fetching the lookup table from memory gets more expensive with size.
1919
2020#### Example
2121
2222Say we have a table with 80 entries ranging from -20 to +70, each containing a 32-bit address, with a few values that don't map onto branches redirected to the **else** block. Since the domain is 90 (70-(-20)), these are indexed sequentially from 0 to 89, with -20 subtracted (i.e. 20 is added) from the input.
2323
2424Initially, a simple comparison is performed to determine if the input falls within the given range of -20 to +70, and jumps to the **else** block if not.
2525
2626``` asm
2727 sub $-20, %input ; min = -20; this will probably be optimised later to "add $20, %index"
2828 cmp $90, %input ; max-min = 70-(-20) = 90
2929 ja @ElseBlock
3030```
3131
3232This also has the added benefit of remapping %input so the smallest case label is equal to zero.
3333
3434Even though %input contains a signed value, the design of two's complement notation is taken advantage of because if %input was initially less than -20, then it will still be negative after the remapping, hence the highest bit will be set and will appear to be a very large unsigned number when evaluated under the rules of 'jump if above'. In other words, the JA command successfully branches if %index is too small or too large.
3535
3636For the actual lookup, it is a simple matter of scaling the input and adding it to the base address of the jump table. For i386:
3737
3838``` asm
3939 mov @TableAddress, %addr
4040 add (%addr,%input,4), %addr ; Note: this assumes that the addresses are relative to the base pointer.
4141 ; If they aren't, this instruction needs to be "mov" instead
4242 jmp *%addr
4343```
4444
4545For x86_64:
4646
4747``` asm
4848 lea @TableAddress(%rip), %base ; 64-bit address.
4949 movs (%base,%input,4), %addr ; Jump addresses are strictly 32-bit, even under x86_64.
5050 add %base, %addr ; Note: this assumes that the addresses are relative to the base pointer.
5151 ; If they aren't, this instruction can be removed
5252 jmp *%addr
5353```
5454
5555If size is not a concern, this could be very slightly optimised by having the jump table sign-extended to 64-bit entries, then code similar to the i386 version can be utilised with one fewer registers used:
5656
5757``` asm
5858 lea @TableAddress(%rip), %addr
5959 add (%addr,%input,4), %addr ; Note: this assumes that the addresses are relative to the base pointer.
6060 ; If they aren't, this instruction needs to be "mov" instead
6161 jmp *%addr
6262```
6363
6464#### Entire Range Covered
6565
6666If the entire range of input values is covered, as is the case with an enumerated type, then the initial remapping and range check can be skipped, and all is required is an offset to the reference. For example, if -20 to +70 is the entire valid range:
6767
6868For i386:
6969
7070``` asm
7171 mov %addr, @TableAddress
7272 add -80(%addr,%input,4), %addr ; min*4 = -20*4 = -80
7373 jmp *%addr
7474```
7575
7676For x86_64:
7777
7878``` asm
7979 lea %base, @TableAddress(%rip)
8080 movs -80(%base,%input,4), %addr ; min*4 = -20*4 = -80
8181 add %base, %addr
8282 jmp *%addr
8383```
8484
8585Just be warned that this code trusts that the input is sane and isn't set to an ordinal value that is outside of the given valid range (as might happen if the input variable hasn't been initialised). Failure to uphold this prerequisite will cause unpredictable behaviour.
8686
8787### Binary Search Lookup
8888
8989Main article: [Binary search algorithm](https://en.wikipedia.org/wiki/Binary_search_algorithm)
9090
9191- Complexity O(log n)
9292
9393#### Requirements
9494
9595- 32 or more branches.
9696- All branches call an identical function. Branches that have multiple case values or ranges are treated as individual branches for each single value.
9797- The range of case labels is large, which would make a direct index or hash lookup impractical.
9898
9999##### Most suitable case
100100
101101A case block that evaluates an ordinal value with many identical branches that share a common header design. Example below taken from **compiler\x86_64\aoptcpu.pas**, lines 68-119:
102102
103103``` pascal
104104
105105{ All subroutines share the form "function(var p: tai): Boolean;" - note that if
106106 one of them is, say "function(const p: tai): Boolean;", this system won't work }
107107
108108case taicpu(p).opcode of
109109 A_AND:
110110 Result:=OptPass1AND(p);
111111 A_MOV:
112112 Result:=OptPass1MOV(p);
113113 A_MOVSX,
114114 A_MOVZX:
115115 Result:=OptPass1Movx(p);
116116 A_VMOVAPS,
117117 A_VMOVAPD,
118118 A_VMOVUPS,
119119 A_VMOVUPD:
120120 result:=OptPass1VMOVAP(p);
121121 A_MOVAPD,
122122 A_MOVAPS,
123123 A_MOVUPD,
124124 A_MOVUPS:
125125 result:=OptPass1MOVAP(p);
126126 A_VDIVSD,
127127 A_VDIVSS,
128128 A_VSUBSD,
129129 A_VSUBSS,
130130 A_VMULSD,
131131 A_VMULSS,
132132 A_VADDSD,
133133 A_VADDSS,
134134 A_VANDPD,
135135 A_VANDPS,
136136 A_VORPD,
137137 A_VORPS,
138138 A_VXORPD,
139139 A_VXORPS:
140140 result:=OptPass1VOP(p);
141141 A_MULSD,
142142 A_MULSS,
143143 A_ADDSD,
144144 A_ADDSS:
145145 result:=OptPass1OP(p);
146146 A_VMOVSD,
147147 A_VMOVSS,
148148 A_MOVSD,
149149 A_MOVSS:
150150 result:=OptPass1MOVXX(p);
151151 A_LEA:
152152 result:=OptPass1LEA(p);
153153 A_SUB:
154154 result:=OptPass1Sub(p);
155155 A_SHL,A_SAL:
156156 result:=OptPass1SHLSAL(p);
157157 A_SETcc:
158158 result:=OptPass1SETcc(p);
159159end;
160160```
161161
162162#### Table Design
163163
164164Internally, each entry of the lookup table contains two ordinal fields equal to the machine word size, totalling 8 bytes for 32-bit and 16 bytes for 64-bit.
165165
166166``` pascal
167167TCaseTableEntry = packed record
168168 Key: NativeInt;
169169 ProcAddr: CodePointer;
170170end;
171171
172172TCaseTable = array[0..ListSize-1] of TCaseTableEntry;
173173```
174174
175175The most important element with this table, as with all binary searches, is that it must be sorted by key in ascending order. Given such case tables are static during runtime, this can be done during compilation using any general-purpose sorting algorithm.
176176
177177#### Iteration Count
178178
179179A binary search divides the eligible list in half with each iteration, meaning its complexity grows with the base-2 logarithm of the list size -- that is, the algorithm is O(log n). While this gives us a rough guide as to the number of times the critical loop will run, it is not an exact answer, and **log2**(*n*) is only integral when *n* is a power of 2. However, if we analyse the workings of the binary search, we can deduce the iteration count:
180180
181-<figure>
182-<img src="BinSearch8.png" title="BinSearch8.png" />
183-<figcaption>BinSearch8.png</figcaption>
184-</figure>
181+![](/wiki-images/0/01/BinSearch8.png)
185182
186183If we take a list size of 8 and evaluate the midpoint (either one of the two middle values will suffice) and determine it is not a match, we can exclude this value and hence are left with one of two sublists to evaluate, one of size 3 and one of size 4. Given the search may take us to the larger of the two, we have to assume worst-case behaviour. Similarly, at size 4, the sublists are of size 1 and 2, and then at size 2, we are left with either a sublist of 1, a match, or a mismatch, since there is no second sublist. At size 1, we just check if this is the value we are looking for, and if not, then we can conclude that the value does not appear in the list.
187184
188185This evaluates to 3 iterations and a final comparison with the 1-sized sublist. This agrees with the value of **log2**(8) = 3. This is the simplest case though where the list size is a power of two. If we go one higher to a list size of 9...
189186
190-<figure>
191-<img src="BinSearch9.png" title="BinSearch9.png" />
192-<figcaption>BinSearch9.png</figcaption>
193-</figure>
187+![](/wiki-images/1/19/BinSearch9.png)
194188
195189Because we can exclude the midpoint, both sublists have a size of 4, and following the worst-case process listed above, this also results in 3 iterations. If we instead reduce the size to 7...
196190
197-<figure>
198-<img src="BinSearch7.png" title="BinSearch7.png" />
199-<figcaption>BinSearch7.png</figcaption>
200-</figure>
191+![](/wiki-images/9/95/BinSearch7.png)
201192
202193Both sublists are now only size 3, and taking the midpoint of one of these sublists yields a pair of 1-sized sublists, thereby reducing the iteration count to just 2 plus the final comparison.
203194
204195From this, we can deduce that the largest sublist size from a list of size n is equal to **floor**(*n*/2). Taken to the logical extreme where the sublist size is an odd number in each division stage, we find that only 3 iterations are required up to an initial list size of 15, since this divides down to 7, then 3, and then we are left with one of two 1-sized sublists that just need a comparison. Such number sequences are all 1s in binary, and this corresponds with values of the form 2<sup>*a*</sup> - 1, one below the argument that causes **log2** to reach its next integral value. Therefore, we can conclude that the total iteration count is a remarkably simple equation:
205196
206197**floor**(**log2**(*n*))
207198
208199This is relatively fast to calculate, as it's equivalent to the position of the most significant 1-bit in the binary value of *n*.
209200
210201#### Loop Code
211202
212203Start with %lo = -1, %hi = number of case labels, %key = value to be searched (assumed to be a signed integer), %table being the address of the map that contains the pointers (which may have to be set beforehand due to the requirement of relocatable code on x86-64), and %mid and %temp undefined. The reason for "lea 2(%lo), %temp" is explained later.
213204
214205``` asm
215206@LoopBegin:
216207 lea 1(%lo,%hi,1), %mid
217208 shr %mid
218209 cmp (%table,%mid,8), %key
219210 cmovge %mid, %lo
220211 cmovle %mid, %hi
221212 lea 2(%lo), %temp
222213 cmp %temp, %hi
223214 jg @LoopBegin
224215```
225216
226217With loop unrolling, this can be simplified further:
227218
228219``` asm
229220 lea 1(%lo,%hi,1), %mid
230221 shr %mid
231222 cmp (%table,%mid,8), %key
232223 je @CaseMatch
233224 cmovge %mid, %lo
234225 cmovle %mid, %hi
235226```
236227
237228... with “@CaseMatch” appearing later. This block of code is repeated for each iteration. If there is concern that the conditional jump may incur a performance penalty, it can be removed without incident, especially during the last couple of iterations (an exact match will set %lo and %hi to the same value).
238229
239230For 64-bit systems, the comparison is more complicated because each entry in the table is 16 bytes, and there is no facility to multiply by 16 in reference operands. Nevertheless, with some rearrangement of the commands to take advantage of a CPU core's multiple ports and the availability of additional registers, this can be resolved relatively painlessly:
240231
241232``` asm
242233 lea 1(%lo,%hi,1), %index
243234 lea 1(%lo,%hi,1), %mid
244235 and $-2, %index ; Clear lsb
245236 shr %mid
246237 cmp (%table,%index,8), %key
247238 je @CaseMatch
248239 cmovge %mid, %lo
249240 cmovle %mid, %hi
250241```
251242
252243During the search, the value of %lo is one below the lowest valid index, while %hi is one above the highest valid index. If at any point that %lo + 1 ≥ %hi, the entire list has been searched. Due to how %mid is calculated, if execution passes through the unrolled loop without jumping out, %lo and %hi will have a difference of 2 and sit either side of the index of the last remaining entry in the list that hasn't been checked, hence one final lookup in the table against %key will confirm a match, with a jump to the case's "else" block (or the end of the block if one isn't present) if it's not.
253244
254245For i386:
255246
256247``` asm
257248 lea 1(%lo,%hi), %mid
258249 shr %mid
259250 cmp (%table,%mid,8), %key
260251 jne @ElseBlock
261252@CaseMatch:
262253 mov 4(%table,%mid,8), %procaddr
263254 ; Do common parameter set-up
264255 call %procaddr
265256 ; Do common result handling
266257 jmp @CaseBlockEnd
267258@ElseBlock:
268259 ; Else block code
269260 ...
270261@CaseBlockEnd:
271262 ...
272263```
273264
274265For x86-64 (note the use of %index in the @CaseMatch setion):
275266
276267``` asm
277268 lea 1(%lo,%hi), %index
278269 and $-2, %index
279270 cmp (%table,%index,8), %key
280271 jne @ElseBlock
281272@CaseMatch:
282273 mov 8(%table,%index,8), %procaddr
283274 ; Do common parameter set-up
284275 call %procaddr
285276 ; Do common result handling
286277 jmp @CaseBlockEnd
287278@ElseBlock:
288279 ; Else block code
289280 ...
290281@CaseBlockEnd:
291282 ...
292283```
293284
294285#### Buffer Overrun Trap
295286
296287One drawback to this highly optimised binary search is that it doesn't trap the situation where the input key is greater than the largest key in the table, and if this happens, the loop may exit with %lo + 1 = %hi = list length, then "lea 1(%lo, %hi), %mid" sets %mid to a value which is out of bounds. To get around this, we recall that in normal operation, %lo and %hi have a difference of 2 if the flow reaches the end of the loop, and are equal if a match was found early, therefore being both odd or both even, their sum is a multiple 2, and so the "+1" part of the LEA instruction has no effect after dividing the sum by 2, so the simple fix is to replace the LEA instruction with "lea (%lo, %hi), %mid", which stops %mid from ever exceeding the upper bound without affecting normal behaviour. In other words, for i386:
297288
298289``` asm
299290 lea (%lo,%hi), %mid
300291 shr %mid
301292 cmp (%table,%mid,8), %key
302293 jne @ElseBlock
303294@CaseMatch:
304295 mov 4(%table,%mid,8), %procaddr
305296 ; Do common parameter set-up
306297 call %procaddr
307298 ; Do common result handling
308299 jmp @CaseBlockEnd
309300@ElseBlock:
310301 ; Else block code
311302 ...
312303@CaseBlockEnd:
313304 ...
314305```
315306
316307For x86-64:
317308
318309``` asm
319310 lea (%lo,%hi), %index
320311 and $-2, %index
321312 cmp (%table,%index,8), %key
322313 jne @ElseBlock
323314@CaseMatch:
324315 mov 8(%table,%index,8), %procaddr
325316 ; Do common parameter set-up
326317 call %procaddr
327318 ; Do common result handling
328319 jmp @CaseBlockEnd
329320@ElseBlock:
330321 ; Else block code
331322 ...
332323@CaseBlockEnd:
333324 ...
334325```
335326
336327#### Putting it all together
337328
338329Now that the registers and exceptional cases are explained, we can put the entire thing together.
339330
340331For i386:
341332
342333``` asm
343334@LoopBegin:
344335 lea 1(%lo,%hi,1), %mid
345336 shr %mid
346337 cmp (%table,%mid,8), %key
347338 cmovge %mid, %lo
348339 cmovle %mid, %hi
349340 lea 2(%lo), %temp
350341 cmp %temp, %hi
351342 jg @LoopBegin
352343 ; Unroll the above if desired
353344
354345 lea (%lo,%hi), %mid
355346 shr %mid
356347 cmp (%table,%mid,8), %key
357348 jne @ElseBlock
358349@CaseMatch:
359350 mov 4(%table,%mid,8), %procaddr
360351 ; Do common parameter set-up
361352 call %procaddr
362353 ; Do common result handling
363354 jmp @CaseBlockEnd
364355@ElseBlock:
365356 ; Else block code
366357 ...
367358@CaseBlockEnd:
368359 ...
369360```
370361
371362For x86-64:
372363
373364``` asm
374365@LoopBegin:
375366 lea 1(%lo,%hi,1), %index
376367 lea 1(%lo,%hi,1), %mid
377368 and $-2, %index ; Clear lsb
378369 shr %mid
379370 cmp (%table,%index,8), %key
380371 cmovge %mid, %lo
381372 cmovle %mid, %hi
382373 lea 2(%lo), %temp
383374 cmp %temp, %hi
384375 jg @LoopBegin
385376 ; Unroll the above if desired
386377
387378 lea (%lo,%hi), %index
388379 and $-2, %index
389380 cmp (%table,%index,8), %key
390381 jne @ElseBlock
391382@CaseMatch:
392383 mov 8(%table,%index,8), %procaddr
393384 ; Do common parameter set-up
394385 call %procaddr
395386 ; Do common result handling
396387 jmp @CaseBlockEnd
397388@ElseBlock:
398389 ; Else block code
399390 ...
400391@CaseBlockEnd:
401392 ...
402393```
403394
404395---
405396*From the Free Pascal wiki, page "Case Compiler Optimization", as it stood on 2019-04-25 after a change by CuriousKit, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
406397Category: Compiler design
407398Category: Proposals

Back to history