Semantically ordered arguments, round 2
Back in 2021, I wrote that “Semantically ordered arguments should be lexically ordered too.” Two minor updates in that area, which are large enough to deserve a post of their own.
clamp
in x86-64 SSE2 assembly
A blogger who goes by “1F604” points out that
“gcc and clang sometimes emit an extra mov instruction for std::clamp on x86”
(January 2024). This is because the SSE2 instructions minsd
and maxsd
are two-operand instructions
which return the value of their non-destination operand in case of equality (e.g., when comparing +0.0
against -0.0
).
My previous post points out (with a hat tip to Walter E. Brown) that the STL makes a similar
mistake with std::min
and std::max
. The STL invariably prefers to return the first
(that is, leftmost) argument value, so that std::min(-0, +0)
is -0
and std::max(-0, +0)
is also -0
.
Unfortunately, on x86-64, these conventions fight with each other:
|
|
std::max
’s semantics require that we invoke maxsd
with a
in the non-destination position, so that
it’ll be preserved in case of equality. But that means that the result of maxsd
ends up in %xmm1
,
so we have to move it back into the return register %xmm0
. From a codegen point of view, we’d rather
max
had Walter Brown’s semantics:
|
|
We have the same problem with std::clamp
, which wants to return the value of its semantically middle
operand in case of equality. E.g., when clamping +0.0
between -0.0
and -0.0
, we want to return
+0.0
. The STL’s std::clamp
is (IMO wrongly) specified to take its semantically middle operand
in first position, so we have this:
|
|
Whereas if it had been specified to take its semantically middle operand in the middle position, we’d have had this:
|
|
However, all of this is contingent on SSE2’s objectively strange choice to
prefer the non-destination operand in minsd
and maxsd
, plus x86-64’s (non-strange)
choice to make the first parameter register the same as the return register in this case.
None of this applies to RISC architectures with no min
or max
opcodes. None of this
applies to SSE2’s successor AVX, either: AVX’s vminsd
and vmaxsd
are three-operand instructions.
This is all just a quirk of SSE2.
std::pointer_in_range(mid, first, last)
P3234 “pointer_in_range
”
(Fernandes, 2024) continues the precedent set by std::clamp
, of putting the semantically “middle”
argument lexically first rather than lexically in the middle. Admittedly there’s implementation
precedent, too: Qt has q_points_into_range(mid, first, last)
and Boost has pointer_in_range(mid, first, last)
.
For pointer_in_range
, switching the parameter order makes no difference to the codegen.
First because parameters of type T*
are passed in general-purpose registers and ISAs tend not to
privilege one general-purpose register above another. For example, x86-64 doesn’t care
whether we ask “Is %rdi
between %rsi
and %rdx
?” or “Is %rsi
between %rdi
and %rdx
?”
And second because when we’re dealing with general-purpose registers, the return register
%rax
isn’t used to pass any of the parameters. So even a no-op function like
[](int x) { return x; }
ends up performing a move.
Would I still prefer the parameters to be passed as (first, mid, last)
instead of (mid, first, last)
?
Yes! But for purely aesthetic and philosophical reasons, not codegen reasons.
Of course, I wrote “Semantically ordered arguments should be lexically ordered too” (2021-05-05)
without realizing that the parameter order affected clamp
’s codegen.
Maybe in another three years I’ll hear how parameter order affects pointer_in_range
’s codegen, too…