-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: extend comma-ok expressions to + - * / arithmetic #6815
Comments
Comment 5 by mdempsky@chromium.org: I like the idea of an extra value to easily check for overflow conditions, but the proposed wording seems to have at least a couple issues when extended to signed integer arithmetic: If x and y are both int32(-1), then (int64(-1) + int64(-1)) >> 32 evaluates to -1. This isn't consistent with the "for +, c is the 'carry' value (0 or 1)" text, as the value is neither 0 nor 1. Additionally, if x and y are both math.MaxInt32 (or both math.MinInt32), then (int64(x) + int64(y)) >> 32 will evaluate to 0 (or -1, respectively). In this case, (x + y) will wrap around, but c won't give any useful indication of that (at least when inspected in isolation). |
I know that the suggestion here was to spare the compiler some work. However, I think that it would now be quite easy to write an SSA pass that recognizes code like q := x / y
r := x % y and substitutes a specialized op for it, and then add faster arch-specific implementations for that op. Something similar probably holds for carry, borrow, and overflow, if we pick a standard idiomatic way to express them (a la the memclear range expression). cc the usual numeric/SSA crowd: @dr2chase @brtzsnr @randall77 @tzneal |
(Picking an idiom to express such calculations also solves @mdempsky's concerns, since it completely specifies the correct behavior.) |
In SSA we have so far avoided having ops which return multiple values. I think such a thing is inevitable, and this is one use case for it. Another is ops which generate both a result and a flags value, so we don't have to do things like SUBQ x, y; CMP $0, y; JNE ... . If we had a subtract that generated a result and flags, we could avoid the comparison. Multiple-value-generating ops require some sort of tuple type and tuple-extraction psuedoops in the SSA form. It isn't hard, but I've avoided it for now because life is simpler without those things. |
That would address the point about compiler efficiency, but that doesn't really address the main point of the issue - which is to simplify and improve readability in code that cares about overflow and remainders. (Adding to the spec is unlikely to spare the compiler much work anyway - and this case is easy enough to optimize using any of a bunch of strategies that don't require extending the language.) |
I don't like coupling the language to the underlying architecture
more than it's absolutely needed.
For example, the proposed
sum, carry := a + b
makes the implicit assumption that the underlying architecture
has the concept of carry bit. What if the architecture doesn't have
carry bit? For those architectures, the proposed form does not
match to the underlying architecture easily, and the compiler is
likely to generate worse code too. As an example, the RISC-V
architecture doesn't contain any flags register or carry bits.
Similarly, the proposed
quo, rem := p / q
assumes that quotient and remainder are available from the same
instruction, but actually most RISC architectures don't offer such
a divide instruction.
It's ok for the compiler to detect that both quotient and remainder
are needed and group that into a single instruction, but let's not
tie the language to x86 or any particular architecture paradigm.
|
Then it compiles to more than one instruction, of course. Same as what you do today if the architecture doesn't have a hardware divide instruction - there's no general requirement that operators in the high-level language be in 1:1 correspondence with machine instructions.
No? It only assumes that the programmer is interested in both the quotient and the remainder of the operation. If you can compile those to a single instruction, that's great - but if you can't, you compile it to a set of instructions that compute them independently. Again, this seems to be missing the point: the main benefit of the proposed syntax is to simplify programs that need carries, remainders, and the like - not to make the compiler's instruction selection easier to implement. |
@minux What is the preferred way to implement multi-precision arithmetic (say + for arbitrarily large numbers) on RISC-V (you mentioned the absence of a carry bit). Is a separate sequence of instructions needed to determine the carry explicitly? (What's the relevant section in http://riscv.org/specifications/?) |
One way to do multiple precision arithmetic without carry is
to use sltu instruction to compare the result of an add with
any of the input operand. It's basically a direct translation
of what you would write with portable C.
|
I still believe there is merit to this idea, especially because we already do have the concept of operations returning one or two results, depending on context. No matter the underlying architecture, the code is nicer to write and a compiler will likely be able to generate better code. The ability for a compiler to recognize patterns that amount to the same effect is orthogonal to this feature. That said, we're not going to make language changes of this magnitude at this point. Marked this for Go2, just so we have a record of this discussion. |
CL https://golang.org/cl/25004 mentions this issue. |
We now allow Values to have 2 outputs. Use that ability for amd64. This allows x,y := a/b,a%b to use just a single divide instruction. Update #6815 Change-Id: Id70bcd20188a2dd8445e631a11d11f60991921e4 Reviewed-on: https://go-review.googlesource.com/25004 Reviewed-by: Josh Bleecher Snyder <josharian@gmail.com> Reviewed-by: David Chase <drchase@google.com>
Just noting http://golang.org/cl/29954 where something like this may be useful. |
As an end-user who never considered overflow conditions in my code, I'd indeed expect an 'ok' return instead of the carry. I see some conflicting goals for this feature:
The order above matches my perspective on importance; I'd accept a loss in performance in exchange of clarity and consistence. That is, for addition it could be implemented in Go directly, not considering the processor architecture:
(Wouldn't a SufficientlySmartCompiler(TM) take these checks and transform it into very optimized machine code already?) Also: When this feature is implemented I can already see golint warning on this construct:
|
FWIW, there's discussion of adding checked arithmetic library routines in #18616. |
Yes. As I see it, that's the main purpose of extending the operators rather than adding a library: programmers view the core language as "the default thing to use" and libraries as "power-user features", and arguably that encourages the wrong defaults in terms of thinking about (and handling) overflow.
I think that's a non-goal? The point of adding language features is to improve programs and/or the programming experience. We don't generally add features to Go just to demonstrate that they could fit in.
Yes, although arguably that goal could be satisfied almost as well with a library.
I think that's a non-goal, too: it would be easy enough to add performance optimizations for specific patterns, especially now that the compiler has an SSA backend. The potential performance implications don't seem nearly strong enough to justify adding a language feature.
Yes. The point of the proposal is to improve legibility and correctness-by-default ― not performance. |
@bcmills, glad we agree on the most important parts. Point 2. is indeed my personal opinion: it'd be awesome to show off, and perhaps bringing people to Go that care about safety. And I'm certainly not serious about the SufficientlySmartCompiler(TM), though I'm curious what's the actual, generated code today. I didn't see this brought up yet: I was thinking a bit more about the issue, and now I believe that division should also return an 'ok', to bubble up the result from intermediate operations. We'd also be able to use that for zero-checking!
So my feeling is now against the proposed change, which is to surface the internals of arithmetic operations (carries, remainder), and stick to the 'ok' convention to instead provide an 'out of bounds/invalid argument' error reporting. |
There's this proposal for extending the language and the proposal (for a proposal) for a library in #18616. Of the two, I'd much rather have it be part of the language. I think we definitely need one of the two, and a library is certainly better than nothing. They both have their pros and cons. (In this I'm assuming that either will perform equally well with compiler intrinsics, and ignoring any particulars up for debate as much as possible). Language change:
Library:
All of that said, and while, again, I would prefer a language change, I think a math/checked library seems more reasonable. † realistically 3 operations (for a maximum of 30 functions) since DivRem isn't symmetric in a math/checked library like it is with the syntax change and the body of that function is trivial, easy to do now safely and tersely, and more amenable to optimization than Add, Sub, and Mul. |
Yeah, I think that's the biggest downside to the language change. But as @griesemer noted (#6815 (comment)), it's something we can revisit for Go 2. At this point, I think the best approach is likely a standard library (or
It is indeed unfortunate that the library would need so many functions. Sadly, the fix for that (#15292) is also labeled for Go 2. |
I'm going to mark this a proper proposal (for Go 2). That said, and even though I originally proposed this, I am less and less in favor of this. The original motivation was to write some of the math/big code routines in Go rather than assembly, and get similar performance. Yet, it's probably always possible to squeeze out more performance in assembly for some of these core routines (about a dozen or so), and if we can, we want to because these routines are crucial (e.g crypto applications). Also, it will be much easier to tweak the specific assembly code than the compiler if a better way to write that code emerges. Finally, the places where code like this matters is small. More philosophically, moving specialized complexity into the language and compiler just doesn't seem justified for these specific routines (it's a bit a different story for built-ins such as |
@griesemer to clarify do you mean that you're marking the proposal for the language change to Go2 (in favor) but that a separate proposal for a math/checked library would be considered? |
@jimmyfrasche I'm less and less in favor of this language change no matter Go 1 or Go 2. There's no way this is going in Go 1, so I leave it open for Go 2, to be considered as a proper proposal (others may feel differently). We have accepted the math/bits library and we are still considering extending it with some of these operations, which would be an an alternative approach. |
In an out-of-band discussion, @praetergeek pointed out that the typical use of a carry value from an addition is to combine it with some later (three-operand) addition: And it's worth noting that you can do around 2^31 additions before the carry bits overflow an So if we were to pursue this proposal further, perhaps we would want to allow strings of multiple addition or subtraction operations, with the second value representing the sum of the carries rather than a single bit. That would also help cut down on boilerplate when the feature is used: applications that are sensitive to overflow in aggregate but don't care about temporary excursions could simply string together multiple operations and check whether the carry is nonzero. It isn't obvious to me whether that approach can be applied to expressions that mix addition or subtraction with multiplication or division. |
I think we should also provide "comma, ok" or carry forms for lossy conversions. For example:
would result in |
I've been thinking about this some more. Putting the carry in the second value has a nice mathematical purity to it, but in practice it seems more useful to be able to detect overflow for an expression containing multiple arithmetic operators (including multiple multiplication operators). So I'm now in favor of using booleans instead (as described in #19624). |
On 26 March 2017 at 06:02, Bryan C. Mills ***@***.***> wrote:
And it's worth noting that you can do around 2^31 additions before the
carry bits overflow an int32.
The alternative of using counting carry will still perform *worse* than the
single-bit carry version, even -- or especially -- when amortized over a
large number of operations.
Addition with with a *single-bit* carry-in and carry-out has zero
additional cost over a regular addition, because it's a single CPU
instruction. Whereas implementing the "counting carry-out" that you're
suggesting would be at least twice as expensive.
The intended use-case is operand-major iteration, rather than
word-within-operand-major. Multi-precision addition would look like this:
a := operands[0] // or make a copy, if you might need to re-use the
first operand
for b := range operands[1:] {
c := false
for i, _ := range b {
a[i], c := a[i] + b[i] + c // carry-in/carry-out, a single CPU
instruction
}
}
rather than
a := operands[0]
c := 0
for i, _ := range a {
a[i], c = a[i] + c
c = 0
for b := range operands[1:] {
a[i], c += b[i] // counting carry, at least 2 CPU instructions
}
}
In the ideal case "operands" is declared as "[][8]uint64" in which case the
inner loop could be unrolled in the first version.
|
FWIW, given both Robert's Mar 20 comment above and the fact that now we have math/bits for similar compiler trickery, it seems like a language change is overkill for this, since these are only rarely needed. It would be much simpler to add appropriate functions to math/bits than to complicate the core language. |
Based on above comments, withdrawing this proposal. |
The text was updated successfully, but these errors were encountered: