Description
Proposal: Permit signed integer values as shift count (language change)
Author: Robert Griesemer
Last updated: 2/15/2017
Abstract
We propose to change the language spec such that the shift count (the rhs operand in a <<
or >>
operation) may be a signed or unsigned (non-constant) integer, or any non-negative constant value that can be represented as an integer.
Background
See Rationale section below.
Proposal
We change the language spec regarding shift operations as follows: In the section on Operators, the text:
The right operand in a shift expression must have unsigned integer type or be an untyped constant that can be converted to unsigned integer type.
to
The right operand in a shift expression must have integer type or be an untyped constant that can be converted to an integer type. If the right operand is constant, it must not be negative.
Furthermore, in the section on Integer operators, we change the text:
The shift operators shift the left operand by the shift count specified by the right operand.
to
The shift operators shift the left operand by the shift count specified by the right operand. A run-time panic occurs if a non-constant shift count is negative.
Rationale
Since Go's inception, shift counts had to be of unsigned integer type (or a non-negative constant representable as an unsigned integer). The idea behind this rule was that a) the spec didn't have to explain what happened for negative values, and b) the implementation didn't have to deal with negative values possibly occurring at run-time.
In retrospect, this may have been a mistake; a sentiment most recently expressed by @rsc here. It turns out that we could actually change the spec in a backward-compatible way in this regard, and this proposal is suggesting that we do that.
There are other language features where the result (len(x)
), argument (n
in make([]T, n)
) or constant (n
in [n]T
) are known to be never negative or must not be negative, yet we return an int
(for len
, cap
) or permit any integer type. Requiring an unsigned integer type for shift counts is frequently a non-issue because the shift count is constant (see below); but in some cases explicit uint
conversions are needed, or the code around the shift is carefully crafted to use unsigned integers. In either case, readability is (slightly) infringed, and more decision making is required when crafting the code (should we use a conversion or type other variables as unsigned integers). Finally, and perhaps most importantly, there may be cases where we simply convert an integer to an unsigned integer and in the process inadvertently make an (invalid) negative value positive in the process, possibly hiding a bug that way (resulting in a shift by a very large number leading to 0).
If we permit any integer type, the existing code will continue to work. Places where we currently use a uint
conversion won't need it anymore, and code that is crafted such that we have an unsigned shift count may not require unsigned integers elsewhere.
An investigation of shifts in the current std library and tests (excluding package-external tests) as of 2/15/2017 (this includes the proposed math/bits package) shows that we have:
- 8081 shifts; or 5457 (68%) right shifts vs 2624 (32%) left shifts
- 6151 (76%) of those are shifts by a (typed or untyped) constant
- 1666 (21%) shifts are in tests (_test.go files)
- 253 (3.1%) shifts use an explicit uint conversion for the shift count
If we only look at shifts outside of test files we have:
- 6415 shifts; or 4548 (71%) right shifts vs 1867 (29%) left shifts
- 5759 (90%) of those are shifts by a (typed or untyped) constant
- 243 (3.8%) shifts use an explicit uint conversion for the shift count
The overwhelming majority (90%) of shifts outside of testing code is by constant values, and none of those turns out to require a conversion. This proposal won't affect that code.
From the remaining 10% of all shifts, 38% (i.e., 3.8% of all shifts) require a uint
conversion. That's a significant number. In the remaining 62% of non-constant shifts, the shift count expression must be using a variable that's of unsigned integer type, and often a conversion is required there. A typical example is (archive/tar/strconv.go:77):
func fitsInBase256(n int, x int64) bool {
var binBits = uint(n-1) * 8 // <<<< uint cast
return n >= 9 || (x >= -1<<binBits && x < 1<<binBits)
}
In this case, n
is an incoming argument and we can't be sure that n > 1
without further analysis of the callers, and thus there's a possibility that n - 1
is negative. The uint
conversions hides that error silently.
Another one is (cmd/compile/internal/gc/esc.go:1417):
shift := uint(bitsPerOutputInTag*(vargen-1) + EscReturnBits) // <<<< uint cast
old := (e >> shift) & bitsMaskForTag
Or this one (/Users/gri/go/src/fmt/scan.go:613):
n := uint(bitSize) // uint cast
x := (r << (64 - n)) >> (64 - n)
Many (most?) of the non-constant shifts that don't use an explicit uint
conversion in the shift expression itself appear to have a uint
conversion before that expression. Most (all?) of these conversions wouldn't be necessary anymore.
The drawback of permitting signed integers where negative values are not permitted is that we need to check for them (negative values) at run-time and (most probably) panic, as we do elsewhere (e.g., for make
). This requires a bit more code in the critical path (an estimated two instructions per non-constant shift: a test and a branch), and it probably will cost extra execution time.
However, none of the existing code will incur that cost because all shift counts are unsigned integers at this point, thus the compiler can omit the check. For new code using non-constant integer shift counts, often the compiler may be able to prove that the operand is non-negative. Furthermore, we can always introduce an explicit uint
conversion, telling the compiler that we know the value is non-negative. This may be the policy of choice in performance-critical code using shifts (e.g., math/bits).
On the plus side, code that used a uint
conversion before won't need it anymore, and will be safer for that since possibly negative values are not silently converted into positive ones.
Compatibility
This is a backward-compatible language change: Any valid program will continue to be valid, and will continue to run exactly the same, without any performance impact. New programs may be using non-constant integer shift counts as right operands in shift operations. Except for fairly small changes to the spec, the compiler, and go/types, (and possibly go/vet and go/lint if they look at shift operations), no other code needs to be changed.
There's a (remote) chance that some code makes use of negative shift count values:
var shift int = <some expression> // use negative value to indicate that we want a 0 result
result := x << uint(shift)
Here, uint(shift)
will produce a very large positive value if shift
is negative, resulting in x << uint(shift)
becoming 0. Because such code required an explicit conversion and will continue to require an explicit conversion, it will continue to work.
Implementation
TBD
Open issues (if applicable)
TBD
Activity
rasky commentedon Feb 15, 2017
Is there any other case in Go where casting a type generates better/worse code? I think it would be surprising to users that
a << b
can generate better code ifb
is changed to an unsigned type.Moreover, this also means that, eventually, there will be more code using
int
as shift count (asint
is more common thanuint
, which is the basic motivating factor of this proposal) and thus paying a price in runtime cost just because most users won't be aware of this nit (in fact, I hope we don't really want to educate everybody on this; the whole idea is to make it more natural).griesemer commentedon Feb 15, 2017
@rasky There are other situations: For instance in
x / y
, wherey
is a power of 2 (y
=1<<k
), we cannot in general replace the division by a simple shiftx >> k
because x may be negative and then the result is incorrect (see https://play.golang.org/p/NfP7MUU4Nt for an example).On the other hand, if we know that
x
is non-negative (for instance because it's an unsigned int), then we can do the strength reduction and replace the division by a shift (or the remainder operation by an AND instruction) in this case.Anybody dealing with performance critical code will already be aware of this (must be aware of the above, for instance, in math/big). For anybody else, this is a net-win because it may actually expose bugs that are now silently swept under the rug via an unchecked
uint
conversion. Finally, I believe that the compiler may be able to prove that a value is non-negative in many situations, a property that is useful for the optimization of other operations.minux commentedon Feb 15, 2017
dongweigogo commentedon Feb 16, 2017
uint is the right choice.
griesemer commentedon Feb 16, 2017
@dongweigogo: Your comment is simply stating a personal preference that is not substantiated. Please provide arguments as to why you believe the status quo is "the right choice".
robpike commentedon Feb 16, 2017
I don't think the problem this solves has been explained well.
The current situation is very clear. Yes, it requires some conversions but in my experience they are not common. For that matter, shifts are not common.
Also, if you permit ints, you allow negative values. Now, you could just panic but many expect a negative shift to reverse the direction of the shift, which is not a good design in practice. People will be confused.
So requiring uint eliminates the consideration completely, which I remember being the main point in Go's decision.
griesemer commentedon Feb 16, 2017
@robpike As you state, requiring uint eliminates the consideration completely, and which is why I also was a strong supporter of the original Go decision. But I am not so convinced anymore that was the best decision looking at actual data.
As it turns out, much of the code (at least in the std lib) that uses non-constants shifts simply uses an unchecked
uint
conversion of the shift count, either right at the shift, or on an expression before the shift that then flows into the shift. This may in fact hide errors. It seems better to have those shift counts checked if necessary and avoid theuint
cast that's only there to satisfy the shift. Again, this only affects 10% of all shifts; the remaining 90% are constant shifts by untyped constant values.rasky commentedon Feb 16, 2017
When you say unchecked conversion, you mean that there is no explicit check that the number is not negative? I don't fully buy this point. If there's some calculation computing a shift amount, your change would allow to catch the subset of bugs which cause the number to be negative (either by really ending up negative, or by becoming negative through overflow); you would still be missing all errors in which the number is just bigger than expected (but not big enough to overflow); so for instance you would still miss most of the off-by-ones, or wrong multiplications by 2/4/8 wrt to the width of the integer being shifted.
I wonder how many hidden bugs of this kind there really are. In fact, shifting by a wrong amount doesn't sound like something that can go unnoticed for a long time, but maybe I just can't picture enough scenarios right now.
I would say that the bug-catching benefit is the one that sounds less convincing to me. The most convincing is making the language simpler for the programmer in the average case (though the runtime cost is hard to swallow -- but I write a lot of performance sensitive code, so I'm sure I'm biased).
griesemer commentedon Feb 16, 2017
@rasky: Answering your respective points:
By "unchecked conversion" I mean that any
uint(x)
conversion will succeed no matter whetherx
actually fits into a uint or not (it may be negative).Of course this doesn't solve arbitrary program errors... - but there is plenty of code where we subtract a small number (1, k, etc.) from a shift count and then convert to
uint
. Those are all cases where the count possibly may end up negative before the conversion (see my examples in the proposal).We don't know how many such hidden bugs exist w/o trying it out, perhaps as an experiment.
Regarding your performance worries: First of all, assuming the std lib is a representative body of code, 90% of all shifts are constant shifts which are not affected. The remaining 10% of all shifts are non-constant shifts. As long as you have a uint shift count (as is always the case now), there is zero performance impact. Even if you move to integer shift counts in your code, often the compiler will be able to prove that the value is always non-negative, in which case no extra check is needed and there will be no performance impact. Finally, in the few cases where there is an extra check, and which are performance-critical, choosing an unsigned shift count will get you back to where you are now at no performance or code cost. With very few exceptions (math/big core routines being an example), the performance difference when switching away from unsigned integer shift counts will be irrelevant to non-existent.
rsc commentedon Feb 16, 2017
I haven't thought a lot about this. It seems like there are three options:
We have 1 now; the proposal above is for 3. I think we should evaluate 2 as well. The rationale would be that if the motivation for dropping the conversion is convenience, then make the change only about convenience, not also a semantic change. That would introduce no new overhead or behaviors.
Not advocating for 2, just saying that we should evaluate it along with 3.
mdempsky commentedon Feb 16, 2017
Another comparison is integer division and modulo, which are implemented with runtime checks for divisor==0. (Though I'm not sure why, since I see there's also code in runtime to handle SIGFPE.)
We also already have to generate extra code for variable shifts, because the machine instructions ignore the higher order bits of the shift register. (I.e., otherwise
x << n
might behave likex << (n % 64)
.)60 remaining items