Skip to content

Fix X86 MulDecomposer performance - #8081

Merged
vijaysun-omr merged 1 commit into
eclipse-omr:masterfrom
zl-wang:PerformanceFixX86MulDecomposer
Jan 21, 2026
Merged

Fix X86 MulDecomposer performance#8081
vijaysun-omr merged 1 commit into
eclipse-omr:masterfrom
zl-wang:PerformanceFixX86MulDecomposer

Conversation

@zl-wang

@zl-wang zl-wang commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

We ran into performance issues when a multiply(by a constant) operation is decomposed into 5 simple operations. On nearly all modern X86 implementations (in the last decade at least), integer multiply instruction has a latency of 3 cycles. Assuming lea/add/sub/shl simple operations having a single cycle latency, you can see why the performance degraded when it is decomposed (usually the sequence has back to back register dependency). Here is a quick fix by conditioning the decomposition on the number of simple operations (although preferably this should be done according to processor specifics).

Also, fix two locations where two ADD(s) are most likely inferior to a single shift.

@zl-wang
zl-wang force-pushed the PerformanceFixX86MulDecomposer branch from 32964c4 to be653e9 Compare January 9, 2026 16:28
@zl-wang

zl-wang commented Jan 9, 2026

Copy link
Copy Markdown
Contributor Author

@vijaysun-omr please review/test/merge. once that is done, i will backport it to 0.57 repo.

@vijaysun-omr vijaysun-omr self-assigned this Jan 9, 2026
@vijaysun-omr

vijaysun-omr commented Jan 9, 2026

Copy link
Copy Markdown
Contributor

I will request a review from @0xdaryl on this, even as I do the testing based on looking at the (relatively safe) changes

@vijaysun-omr

Copy link
Copy Markdown
Contributor

Jenkins build all

;
}

if (j > 3) { // counting from 0, this means there are more than 3 decomposed operations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this "3" should be named via an macro constant, so it appears to be less "magic".

Having the ability to change this value via an env var to experiment would also be handy

@zl-wang

zl-wang commented Jan 12, 2026

Copy link
Copy Markdown
Contributor Author

@0xdaryl there might be some historical background re X86 codegen doing multiply-by-4 through add twice. in general, add twice is slower than a single shift-left-by-2. please provide the historical reason of doing that and whether it is still relevant, before i make these "corrections".

@zl-wang
zl-wang force-pushed the PerformanceFixX86MulDecomposer branch 6 times, most recently from aa55963 to 4c3fd1f Compare January 13, 2026 00:08
@zl-wang
zl-wang force-pushed the PerformanceFixX86MulDecomposer branch from 4c3fd1f to dba4610 Compare January 13, 2026 15:33
@vijaysun-omr

Copy link
Copy Markdown
Contributor

I measured this on multiplication by 52 which is what I saw in one of our benchmarks. While performance was much better than the original sequence, it was still slower by 1-3% it looked like, vs the performance with the env var set to disable all this logic and just just an x86 mul instruction. Maybe we should be using the decomposer logic only for constants that would need less than 3 instructions ? @zl-wang and @0xdaryl opinions are needed for choosing the default cutoff for the number of instructions in the end.

@zl-wang

zl-wang commented Jan 13, 2026

Copy link
Copy Markdown
Contributor Author

Two possible approaches going forward:

  1. simpler: eliminate all cases where 3-instr sequence is used;
  2. complex: need detailed knowledge of instr issuing on x86 implementations, how many each type of unit are available in the core, and the specific 3-instr sequence has the potential to be issued in parallel concurrently. then, keep those 3-instr cases (expecting to outperform mul op itself most likely).

@0xdaryl

0xdaryl commented Jan 13, 2026

Copy link
Copy Markdown
Contributor

there might be some historical background re X86 codegen doing multiply-by-4 through add twice. in general, add twice is slower than a single shift-left-by-2

This code really needs to be looked at through a lens from 2003, when it was first tuned and contributed and then largely left alone for the past 20 years. In the Pentium 4 days, immediate shifts were more expensive than adds. I looked back at some historical documentation and found that SHL by a constant had a latency of 4-5 cycles, but a single ADD register is only a 0.5-1 .5 cycles (therefore two adds are cheaper than a single constant shift). This might explain some of the tuning decisions here.

I think a handful of people may have poked at bits of this code over the years, but a comprehensive review of the tuning decisions for modern x86 processors wasn't done and seems warranted.

@vijaysun-omr

Copy link
Copy Markdown
Contributor

I suggest a staged approach where

simpler: eliminate all cases where 3-instr sequence is used;

is done first. @zl-wang could you please make this change and ready this PR for delivery ?

@zl-wang

zl-wang commented Jan 14, 2026

Copy link
Copy Markdown
Contributor Author

sure ... let me get onto it today or tomorrow

@zl-wang

zl-wang commented Jan 14, 2026

Copy link
Copy Markdown
Contributor Author

@r30shah @knn-k codegen modernization re mulDecompose might be worth of another look. i will review p codegen soon in this regard.

@zl-wang
zl-wang force-pushed the PerformanceFixX86MulDecomposer branch 7 times, most recently from 20fba2d to c62e70e Compare January 14, 2026 21:51
@zl-wang

zl-wang commented Jan 15, 2026

Copy link
Copy Markdown
Contributor Author

I used the C program below and gcc -O3 to verify our mul decomposition strategy: (compare what is generated for each mul operation)

void test(int *array, int x)
{
	array[0] = x*3;
	array[1] = (x+1)*5;
	array[2] = (x+2)*6;
	array[3] = (x+3)*9;
	array[4] = (x+4)*10;
	array[5] = (x+5)*11;
	array[6] = (x+6)*12;
	array[7] = (x+7)*13;
	array[8] = (x+8)*7;
	array[9] = (x+9)*14;
	array[10] = (x+10)*15;
	array[11] = (x+11)*17;
	array[12] = (x+12)*18;
	array[13] = (x+13)*19;
	array[14] = (x+14)*20;
	array[15] = (x+15)*21;
	array[16] = (x+16)*22;
	array[17] = (x+17)*23;
	array[18] = (x+18)*24;
	array[19] = (x+19)*25;
	array[20] = (x+20)*26;
	array[21] = (x+21)*27;
	array[22] = (x+22)*28;
	array[23] = (x+23)*29;
	array[24] = (x+24)*30;
	array[25] = (x+25)*31;
}

a few observations:

  1. it is generally true that gcc cut off decomposition at 3 instructions (imul instruction is used in those cases directly);
  2. in all cases, we are doing equally well or slightly better (for *11, *15, *19 cases. gcc chose to have a reg mov -- resulting in 3 instr in total, whereas we remain 2).

@vijaysun-omr

vijaysun-omr commented Jan 15, 2026

Copy link
Copy Markdown
Contributor

Thanks, the current implementation does quite well in my runs on some relevant benchmarks. I have asked for further confirmation using the state of this PR as it stands. If that also checks out, I feel it should be ready for a final review from @0xdaryl

@0xdaryl

0xdaryl commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

Jenkins build all

@vijaysun-omr

vijaysun-omr commented Jan 20, 2026

Copy link
Copy Markdown
Contributor

@0xdaryl is this ready for merging ? Please go ahead when you are ready

@0xdaryl 0xdaryl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the comment removal, I think this is fine.

// _cg);
// }
// } else
if (shiftAmount != 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the commentary and the commented-out code? As mentioned in an earlier PR comment, this was a point-in-time decision based on the technology at the time that people have tinkered with over the years for various hardware evolutions.

I think it is fair to remove it and just use your approach by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

We ran into performance issues when a multiply(by a constant) operation
is decomposed into 5 simple operations. On nearly all modern X86
implementations (in the last decade at least), integer multiply
instruction has a latency of 3 cycles. Assuming lea/add/sub/shl simple
operations having a single cycle latency (all true from web queries),
you can see why the performance degraded when it is decomposed (usually
the sequence has back to back register dependency).

All decomposition solutions where no performance benefit is
expected are deleted.

In a few solutions, the sequences were made better (shorter).

Also, general Add sequence for shifting is disabled.

Adding a better testing condition (missed in the original code to
begin with).

The condition to use the solutions available is tightened with
considerations of eventual performance benefit.

Comment and disabled code are removed.
@zl-wang
zl-wang force-pushed the PerformanceFixX86MulDecomposer branch from c62e70e to bce5c2a Compare January 21, 2026 13:31
@0xdaryl

0xdaryl commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

Jenkins build xlinux,win,osx

@vijaysun-omr
vijaysun-omr merged commit 373ed1e into eclipse-omr:master Jan 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants