Missed optimizations: Err returns should be out of line, loop not recognized as iterating at least once
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
As of today (2024-11-29), neither the release nor the nightly compiler
does an ideal job of optimizing this string validation function.
pub enum IdError {
IdEmpty,
IdTooLong,
IdNotAscii,
}
#[inline(never)]
pub fn check_id(value: &str, limit: usize) -> Result<(), IdError> {
if value.is_empty() {
return Err(IdError::IdEmpty);
}
if value.len() > limit - 1 {
return Err(IdError::IdTooLong);
}
if value.as_bytes().iter().any(|c| !(1u8..=127u8).contains(c)) {
return Err(IdError::IdNotAscii)
}
Ok(())
}
There are two missed optimizations. For code like this, the compiler
would ideally recognize that conditional Err returns are unlikely,
and make the fall-through path be the one that (eventually) returns
Ok. Nightly gets this right for the first two checks but not the
third. Second, the compiler does not notice that, if the first two
checks succeed, then the loop in the third check must iterate at least
once. In addition to the redundant test, this might be inhibiting
vectorization of the loop.
The dec/cmp/jb sequence used for the second test can also be
micro-optimized to cmp/jbe but that’s extremely minor.
assembly generated by 1.85.0-nightly (2024-11-28 a2545fd6fc66b4323f55)
playground::check_id:
testq %rsi, %rsi
je .LBB0_1
decq %rdx
movb $1, %al ; 1 = Err(IdTooLong)
cmpq %rsi, %rdx
jb .LBB0_8
xorl %eax, %eax
.LBB0_4:
cmpq %rax, %rsi
je .LBB0_5
cmpb $0, (%rdi,%rax)
leaq 1(%rax), %rax
jg .LBB0_4
movb $2, %al ; 2 = Err(IdNotAscii)
.LBB0_8:
retq
.LBB0_1:
xorl %eax, %eax ; 0 = Err(IdEmpty)
retq
.LBB0_5:
movb $3, %al ; 3 = Ok(())
retq
hand-optimized desired assembly (no vectorization)
playground::check_id:
xor %eax, %eax ; 0 = Err(IdEmpty); recycled as index register
test %rsi, %rsi
je .L3
cmp %rsi, %rdx
jbe .L2
.L1:
cmpb $0, (%rdi,%rax)
jng .L4
inc %rax
cmp %rax, %rsi
jb .L1
mov $3, %al ; 3 = Ok(())
ret
.L2:
mov $1, %al ; 1 = Err(IdTooLong)
.L3:
ret
.L4:
mov $2, %al ; 2 = Err(IdNotAscii)
ret
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the issue with the linked Rust Playground on the stated nightly and release configurations, then inspect the generated assembly for check_id. Compare the control flow with the hand-optimized assembly in the report; done means the compiler removes the unnecessary loop-entry check and produces the intended fall-through error/Ok paths without regressing the existing checks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100