Change:
When Firefox switched from Rust 1.24.0 to Rust 1.25.0, the win32 performance of encodingrs's UTF-8 validation function when used on ASCII input. encodingrs's UTF-8 validation function is a fork of the Rust standard library validation function that replaces the ASCII acceleration ALU trick that autovectorizes on x8664 but not on i686 and works only in the aligned case with explicit SIMD code that deals with both the aligned and unaligned cases. When the input is all ASCII, the function should stay in either the aligned-case or the unaligned-case inner loop that loads 16 bytes using or , respectively, performs on the xmm register and compares the result to zero jumping back to the start of the loop if it is zero. When compiled for i686 Linux with opt level 2 (which Firefox uses) using Rust 1.24.0, the result is exactly as expected. Unaligned: Aligned: (Windows wouldn't let me see the asm due to LLVM deeming the IR invalid with .) When compiled with Rust 1.25.0, the result is more complicated: There are two instances of and two instances of suggesting that the first trip through the loop has been unrolled to be a separate copy from the loop proper. In the actual loop, ALU instructions have been moved around including placing one between the SSE2 instructions. Both of these transformations look like plausible optimizations, but considering the performance result from Firefox CI, it seems these transformations made performance worse. The asm was obtained by compiling (Firefox uses 0.7.2) using and searching for in the file.
Note that I observed something similar on x86-64 (and Firefox has seen a regression on x86-64 too).
The loop is inlined from .
The presumed unrolling of the first loop trip happens before LLVM, but the inner loop (which has to be what's slow to get the 12.5% drop in perf) looks the same on the IR level, so this is probably an LLVM 4.0 vs. LLVM 6.0 thing. IR from 1.24.0: IR from 1.25.0:
Code generated by the current nightly looks like the code generated by 1.25.0.
does not fix this.
cc
What instruction scheduling model does rustc use by default? The change looks like it's a result of instruction scheduling trickery, and the LLVM 6.0 release notes say: "Added instruction scheduling information for Intel Sandy Bridge, Ivy Bridge, Haswell, Broadwell, and Skylake CPUs." Notably, when running on Haswell outside the Gecko context (but with ), I was unable to reproduce the performance degradation on the x86_64 Linux target. How can I choose an instruction scheduling model without also enabling the instruction set extensions of the named microarchitecture?
My guess (can't check atm, not at a computer) is that instruction scheduling is chosen by LLVM depending on the target cpu that is specified in the target files and for x8664 that is the most conservative option of... x8664 (no particular cpu, and definitely not haswell+). My guess would be that within gecko context flags that may influence this selection (-Ctarget-cpu -Ctarget-feature etc) are being set to some value that correspond to a more recent cpu.
As far as I can tell, Gecko doesn't set or using the flag. LLVM seems to have microarchitecture-specific sceduling models from Sandy Bridge upwards, and between LLVM 4.0 and 6.0 Intel engineers landed major rewrites of the Sandy Bridge and Haswell scheduling models citing information obtained from the architects of these microarchitectures. It's unclear to me where the default x86 and x86_64 scheduling models reside in the LLVM source.
The code generated for x86_64 for the unrolled innermost loop looks closer to expected, though. Aligned case:
It seems that for the targets, the base CPU is . Changing it to does not help. However, changing it to , which is the base CPU for the targets results in similar instruction scheduling as when actually targeting 64-bit code. (Changing it to without also changing results in different instruction selection.)
I checked old nightlies. The change occurred exactly when rustc switched from LLVM 4.0 to 6.0. Effect 1., the unrolling, happens on the IR level regardless of . Effect 2. seems to be tied to CPU model-specific instruction scheduling for Pentium 4 and Pentium M at least, so effect 2. can be avoided by picking a different . Does rustc expose controls for omitting certain LLVM IR transformation passes? How should I go about locating the cause of effect 1.?
Here are the LLVM passes (64-bit target) from the and the . I'm not familiar with LLVM internals, so the only thing that stands out to me is that the Natural Loop Information pass seems to have moved relative to other passes in some cases and in some cases there are additional Machine Natural Loop Construction passes.
Remedying effect 2 isn't much of a remedy . It seems implausible that the unrolled one trip through the loop could be so slow as to explain the original slow-down, so I assume that the slow-down has to be attributable to the unrolling causing movement of code within the inner loop--specifically causing the index-based loop exit criteria to move to above the buffer content check in the basic block forming the inner loop.
The LLVM 4.0 to 6.0 switch happened between nightly-2018-02-10 and nightly-2018-02-11.
do you have the data necessary to reproduce the runtime regression here? I managed to extract into a so we can hopefully send it to upstream LLVM when we've reduced it enough, but with some cursory testing I'm unable to reproduce the time regression seen here.
Thank you. I even further, tweaked the benchmark to match Firefox memory alignment and iterations, scripts to run the code easily and managed to the regression in x86_64 mode on two different microarchitectures.
I filed an .
You might want to attach some LLVM IR samples to the the LLVM bug report - or even a C(++) source, if it's easy enough to do - to make it easier for LLVM developers to test potential fixes. (In my experience, LLVM developers don't tend to try LLVM changes with custom Rust builds, but they can easily have within their LLVM build tree to quickly try various C(++) testcases.)
I've with a pure C example to help the LLVM devs dig into
This is caused by the loop rotation pass. Using the IR running shows the issue. It's somewhat fragile though, e.g. won't work. On LLVM trunk also no longer seems to exhibit the problem.
I haven't tested, but I suspect that backporting would fix this regression. The problem is as follows: Loop rotation does not happen if the (unique) loop latch is loop exiting (with some exceptions). At the point where loop rotation occurs, the loop latch is an unconditional branch from an empty block. Without indirection through this dummy block, the loop latch would be loop exiting. Prior to the commit referenced above, CFG simplification with canonical loop preservation was not able to remove this extra block.
nice =) Maybe someone can take a stab at evaluating that?
I can try that later tonight, but I feel like I'm going to come back and it will have been done already hah.
Verified that cherry-picking the commit fixes the regression.
Thank you! I verified that the performance of the test case improved with the latest nightly. Is this kind of LLVM cherrypick eligible to be nominated for cherrypicking into Rust beta? How does one nominate beta backports?
I've the beta-nominated tag to which is how we process backports (the compiler team is also tagged to come up during triage)
The backport seems to be causing issues on beta that don't occur on nightly -- the origins of this are not known, perhaps some other fixes that landed in the meantime? (The failing test is myriad-closures, and it fails only on 32-bit windows, afaik -- it may be a memory usage issue?) how much do you care that this is backported? We're trying to decide how many resources to devote to tracking this down vs fixing other things.
Firefox is not updating its compiler from 1.24 () in part because of this. ( can say more about the other blocker.)
is indeed a stress test, and memory usage changes make sense.
Firefox is going to stay on 1.24 until it can jump to 1.28. So, for Firefox, it doesn't matter at all that this makes it to beta.
where's the discussion/rationale on that? I searched Bugzilla but couldn't find it.
beta is 1.27, and 1.27 doesn't allow to get the allocation size on oom, and that's important to us. 1.28 does.
, given what said about jumping to 1.28 anyway, I no longer care about a backport.

Discussion:
<del> Subproject commit 1abfd0e562cc8f7a9577d97ee92246699093b954 </del> <ins> Subproject commit 56c931901cfb85cd6f7ed44c7d7520a8de1edf97 </ins>

# If this file is modified, then llvm will be (optionally) cleaned and then rebuilt. # The actual contents of this file do not matter, but to trigger a change on the # build bots then the contents should be changed so git updates the mtime. <del> 2018-04-05 </del> <ins> 2018-05-18 </ins>  No newline at end of file