Skip to content
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

+= and friends should be type-stable #11971

Closed
nstiurca opened this issue Jul 1, 2015 · 23 comments
Closed

+= and friends should be type-stable #11971

nstiurca opened this issue Jul 1, 2015 · 23 comments

Comments

@nstiurca
Copy link
Contributor

nstiurca commented Jul 1, 2015

Steps to reproduce:

i = uint8(13)   # or 'i = 13%Uint8' in Julia v0.4
i += 2
typeof(i)

Expected results:

Uint8

Actual results:

Int64

Rationale:

Although it makes sense that constant integers default to Int64 (on 64 bit machines), and also that Uint8 + Int64 yields an Int64, operations such as +=, *=, etc. imply that only the value of the variable on the left-hand-side changes, and not its type. The current type instability of these operators can cause mysterious performance issues and other problems, such as incorrect results if the code relies on unsigned-modulus arithmetic.

Discussion

Enforcing type stability for these operators implies that some operations like /= shouldn't be defined with Integer left-hand side. Alternatively, they could be defined in terms of integer division, but that could cause confusion since / normally promotes to Real.

If there is a compelling reason for keeping the current type-unstable behavior, then perhaps Julia can at least issue a warning when it detects this case.

v0.3 vs v0.4

In v0.3, += always promotes the left-hand-side to machine-length integer, even if both operands are eg Uint8. In v0.4, the left-hand-side is type-stable if the right-hand-side is explicitly of the same type; ie, i += 1 % Uint8 is a work-around for incrementing i when i is Uint8. However, I think this is undesirable due to (a) being a weird gotcha of the language people have to watch out for and (b) it being too verbose for such an idiomatic operation as increment.

@quinnj
Copy link
Member

quinnj commented Jul 1, 2015

Please see extensive past discussions on this topic:

After reading up and there's still a concrete change you'd like to see, feel free to ping here and we can reopen or open up a new issue.

@quinnj quinnj closed this as completed Jul 1, 2015
@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

What would you do here for user types that aren't closed under addition? x += y is lowered in a pretty straightforward way to x = x + y, and it's not very likely that will change without a very good rationale. Type stability is an informal case-by-case property that's very difficult to rigorously enforce.

@JeffBezanson
Copy link
Sponsor Member

Yes, the key is that x += y is equivalent to x = x + y. We could instead lower it to x = oftype(x, x + y), but doing more things behind your back tends to be more surprising.

@nalimilan
Copy link
Member

One could also argue that having x += 1preserve the type of x would be a good idea in a language where changing the type of a variable is likely to be a performance disaster. And if oftype(x, x + y) failed because conversion does not make sense, then that code probably deserves refactoring to make it clearer.

Maybe the fact that this kind of request keeps being reported indicates that people think of x += 1 as something slightly different from x = x + 1.

@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

Not all code is performance-critical.

people think of x += 1 as something slightly different from x = x + 1.

That could be considered a failure of documentation.

@johnmyleswhite
Copy link
Member

That could be considered a failure of documentation.

Agreed. This is just an important difference between Julia and other languages that we need to educate people about better.

@nalimilan
Copy link
Member

What I meant is that the idea that x += 1 is simply x = x + 1 may not be as useful as we think if people generally expect another behavior. If that's the case, we may as well choose an apparently more complex definition, which might have be more practical in typical use cases.

Note that I'm not saying such a thing as an alternative "intuitive behavior" of += actually exists. But it might be worth investigating.

@ScottPJones
Copy link
Contributor

The rewriting of x op= expr to x = x op (expr) is fine for immutables, but is not what you want if x is mutable. I think that is what people keep stumbling over.

@nstiurca
Copy link
Contributor Author

nstiurca commented Jul 1, 2015

@quinnj ,
Thank you for those references. I think I am basically of the same opinion as @GunnarFarneback who commented in #9162

My opinion is that if you have an integer type T other than the default (in the sense of unadorned literals) Int, it is probably for a reason, and you want operations between T and T to stay in T, but likewise for operations between T and Int to stay in T.

So the change I would like to see is for literals such as 1 to assume a type appropriate to their context instead of always defaulting to Int64. Likewise for floating types: if x is a Float32 and y is the literal 2.5, then currently you end up with a Float64. In the case of floats, the performance impact is probably even bigger than for integers.

@JeffBezanson ,
I agree with your point that "doing more things behind your back tends to be more surprising". It certainly would be surprising (to me at least) if x += y is not the same as x = x + y. However, silently promoting the Uint8 to Int64 is already something being done behind the scenes which I argue does not really make sense if I've gone through the trouble of specifying Uint8 in the first place.

@nalimilan ,
Interesting discussion on whether x += y is the same as x = x+y or not. People with a C/C++ background expect that they be the same for primitive types, and they have to define it themselves however they see fit for user types. Perhaps in the case of x or y being a user type, Julia can default to x = x + y, but have the option to re-define x += y explicitly. Then the user can get whatever behavior makes most sense in his particular application.

@nstiurca
Copy link
Contributor Author

nstiurca commented Jul 1, 2015

One other interesting question to consider with my proposed change is how to handle constants. Consider

circ_area(r) = 0.5π * r^2

Ideally, I would love to see the return type of this function be the same as the type of r, without having to define different methods for each of Float16, Float32, Float64. Also, the intermediate computation should use the appropriate type, ie without converting r to Float64, doing 64-bit math, then converting back to Float32.

I realize as I type this that my issue title is somewhat misleading since mostly I'm worried about how literals/constants are handled since that is what ultimately leads to the type instability in v0.4.

@quinnj
Copy link
Member

quinnj commented Jul 1, 2015

@nstiurca, I don't think your cases are as confusing in general when you're more familiar with the types of Julia literals. typeof(1) == Int; typeof(2.5) == Float64. So there's nothing that sneaky with

x = UInt8(1)
x += 1
typeof(x) == Int

Because you're adding an Int to a UInt8 which will promote to Int. This is a much different, and much less confusing, IMO, than the previous behavior of

julia> x = int8(1)
1

julia> y = int8(1)
1

julia> x + y
2

julia> typeof(x + y)
Int64

@timholy
Copy link
Sponsor Member

timholy commented Jul 1, 2015

Personally, I think the oftype suggestion has a certain amount of merit.

@ScottPJones
Copy link
Contributor

I also think that the oftype suggestion is better than what it does now.

@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

And for types that aren't closed under addition, += becomes an error?

@timholy
Copy link
Sponsor Member

timholy commented Jul 1, 2015

Runtime error on overflow, yes. I think we have to have that if we adopt the proposal.

A negative is that x += 1 and y = x + 1 will then have different performance characteristics, if the former temporarily changes type (because it would use checked arithmetic, whereas the latter uses unchecked). When x changes type (the only time those two will differ), we have that situation now too, for different reasons. In either case the performance difference would go away by writing x += oftype(x, 1). So in the end both options do kind of get you to the same place.

@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

I'm not sure I would call it overflow for a custom type that cannot represent the result of addition in its data structure, and needs to promote to a separate higher-level container.

@nstiurca
Copy link
Contributor Author

nstiurca commented Jul 1, 2015

@timholy Did you mean oftype rather than typeof in your last comment? Would it be so terrible for literals to always be implicitly wrapped with oftype when used in arithmetic expressions? Assuming the implementation is feasible, the only con would be educating users about the fact that literals have context-sensitive type since I don't think it is common in other languages. The main pro would be that I think Julia would end up doing the right thing in vast majority of cases, including some common ones that currently require verbose workarounds.

@timholy
Copy link
Sponsor Member

timholy commented Jul 1, 2015

Yes, typo.

The proposal to auto-type literals is interesting but not something that can be accomplished with current machinery. As with the oftype proposal, there are negatives as well as positives. In particular, 1 currently means an Int wherever it appears, and that would no longer be true.

@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

You also aren't guaranteed to be able to represent literals in the same data structure as every user type.

@timholy
Copy link
Sponsor Member

timholy commented Jul 1, 2015

I'm not sure I would call it overflow for a custom type that cannot represent the result of addition in its data structure, and needs to promote to a separate higher-level container.

Neither would you want to use x += obj, given the performance hit of type-instability.

Anyway, I don't care strongly one way or another, and have other more pressing duties.

@tkelman
Copy link
Contributor

tkelman commented Jul 1, 2015

Neither would you want to use x += obj, given the performance hit of type-instability.

No, and this would be just as slow as x = x + obj. As I said,

Not all code is performance-critical.

I'm not expecting this data structure to be high-performance, it's intended for convenience. Having x += obj error because oftype doesn't work is not particularly convenient.

@StefanKarpinski
Copy link
Sponsor Member

You can already write the circle area formula like this and get what you want:

julia> circ_area(r) = r^2*π/2
circ_area (generic function with 1 method)

julia> circ_area(1.5)
3.5342917352885173

julia> circ_area(1.5f0)
3.5342917f0

julia> circ_area(big(1.5))
3.534291735288517393270473806189440744721815574296994048596812666346293457071964

If the input is Float16 you get a Float64 but that's because Float16 isn't a computational type.

@StefanKarpinski
Copy link
Sponsor Member

The proposal to auto-type literals is interesting but not something that can be accomplished with current machinery.

I don't think this is just a matter of machinery, I suspect it cannot be done coherently without having rules that statically determine the types of all expressions, which would make the language statically typed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants