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

Should sum() of 1-element array return a new object? #9669

Closed
GregPlowman opened this issue Jan 7, 2015 · 10 comments
Closed

Should sum() of 1-element array return a new object? #9669

GregPlowman opened this issue Jan 7, 2015 · 10 comments

Comments

@GregPlowman
Copy link
Contributor

Consider the following code:

type MyType
    x::Int
end

A = [ MyType(5) ]
sum(A)
sum(A) === A[1]    # true

sum() returns a reference to A[1] rather than a new object.

Also note that for 1-element arrays, sum() works even without +(::MyType, ::MyType) being defined.

See discussion here: https://groups.google.com/forum/?fromgroups=#!topic/julia-users/eh1OVunzYsw

Here's lines 44-47 of reduce.jl:

r_promote(::AddFun, x::Number) = x + zero(x)
r_promote(::MulFun, x::Number) = x * one(x)
r_promote(::AddFun, x) = x
r_promote(::MulFun, x) = x

Possible fix is to take r_promote() currently defined for Numbers only, and use for all types:

r_promote(::AddFun, x) = x + zero(x)
r_promote(::MulFun, x) = x * one(x)

Any type using sum() would need to define +(T, T), zero(T) and zero(Type{T}), which seems reasonable.

@andreasnoack
Copy link
Member

Have you tried to make the changes and run the tests?

@nalimilan
Copy link
Member

The solution suggested above is elegant, but it has a cost if doing x + zero(x) incurs the allocation of an object for zero, and of another one for the sum. This can be the case for custom types, which are not necessarily immutable (IIUC @GregPlowman's use case).

Thus, another solution would be to copy x only when n == 1 in _mapreduce(). This would save some copying in the most common case where n > 1.

@GregPlowman
Copy link
Contributor Author

I agree that there is seemingly unnecessary allocation for n==1.
For n==0 it's slightly worse, summing appears to allocate 3 objects - zero(T) + zero(Type(T))
However, for n > 1, is r_promote() called at all?

Having said that, I'm definitely for a more efficient solution.

@JeffBezanson
Copy link
Member

See the discussion towards the end of #8759.

In many cases, not just sum, our more mathematical code adopts a functional style that is not concerned with whether objects are copied, as they are assumed to have mostly immutable semantics. Summing mutable objects is a bit odd.

It's very hard to convince me that sum needs to go out of its way to try to copy anything. To see how this starts to get out of hand, consider the proposed x + zero(x). An implementation of + for big objects like arrays or BigInt might very well check that y==0 in x+y, and return x in that case to avoid allocation. Library routines cannot do anything to ensure that all surrounding uses of mutation behave as expected. Somebody else might want sum of one element to return the same object.

@GregPlowman
Copy link
Contributor Author

Jeff,

I agree that one can't please everyone, or even someone all of the time.
We might, at different times, want sum() to return a reference, and at other times to return a new object.

Suppose MyBigType implements x + y as you describe, by checking if y==0, and if so returning x (a reference).
If the proposed change is implemented and r_promote(::AddFun, x) = x + zero(x), then indeed a call to sum() with a 1-element array of MyBigType will return a reference to x.
Because + was defined this way.
So the library function is general, leaving it up to the caller and definition of the Type how to handle + and associated sum().
Isn't this more general than forcing a reference return? (more on this later)

Admittedly, this might be inefficient since a temporary object zero(MyBigType) would be created despite not being used. However, this is an implementation detail rather than a functional one.
Theoretically, if we had a lazy implementation of zero(), allocation could be avoided. (Not sure if lazy is the right term here, but I think you can guess what I mean).
In any case, if efficiency was really a concern for summing large mutable types, user would probably write a custom in-place sum!() function to avoid allocation altogether, not just for the rare case where n==1. sum() is inherently inefficient for these large types, so it seems strange to be overly concerned about the 1-element case.

I don't think of the proposed change as an explicit copy that sum() is going out of its way make, but rather converting the return type of sum() to the type of x + y (as defined by the element-type of the array being summed).
The return type and whether it is a reference or copy should be determined by the type.
This is general and functional, not forced by an arbitrary decision in the library code.

Perhaps the type is not closed under addition. In this case returning x + zero() makes more sense.
Probably this is why the implementation of immutable Number is so. Why shouldn't it be for other types as well?

Doesn't consistency of return type count for anything?
sum() returns the result of + for n==0 and n>1, but returns a reference to the single element for n==1.

I do agree with your arguments in #8759 about indiscriminate copying and the dangers of library code trying to determine if and when to copy.
However the proposed change is not, in my opinion, explicit copying inserted arbitrarily into library code.
Rather, I was thinking it is a functional and reasoned use, allowing consistency for varying size arrays, and allowing generality so that calling types can determine return type and whether it is a reference or new object.

Actually for my purpose, I just subtype from Number to get the functionality I need.
So I'm not really pushing for this change. Just wanted to explain an alternative, probably naïve, view.

Cheers, Greg

@JeffBezanson
Copy link
Member

Wanting sum to compute x + zero(x) in order to get a possibly different, or different-type answer, is a totally different issue! If there are good examples of where that would be helpful, by all means let's discuss them.

But if the only real reason to do x + zero(x) is copying, then I would consider that almost obfuscated. You have to go through even more steps of reasoning to know whether it's "safe" to mutate the result of sum. Or say we change our minds about the semantic need for x+zero(x). Will we remember that the "real" goal was copying?

@GregPlowman
Copy link
Contributor Author

For me, the real goal is not copying per se.

The real goal is consistency of return types for sum(), and allowing the "calling" type to control this.
I don't want to think about internals. I use sum() as a convenient library-supplied function, and want consistency and predictability where possible. I want to know there are no nasty surprises. I don't want to think about special cases where array has n==1 or n>1 elements. Nor do I want to code around the differences.

I concede this objective might unrealistic, or at odds with other goals and design philosophies.

I also get that if I really want a copy I should do copy(sum()) or similar. However, it seems users do have or develop some understanding of internals and then start relying on that. (I'm not saying this is good, rather just that it happens). So a user, especially an inexperienced one like me, (wrongly or naively) "knows" and then "expects" that + returns a different object (usually), and so assumes sum() will also.

Please note that I'm merely explaining a newcomers perspective to you, not trying to convince a developer of what's "right".

Again, I'm not really invested in this change. However, it does seem that I have been invested in the explanation of my original query on julia-users, (from which I received generous help that was much appreciated, and culminated in advising me to raise an issue here)

You have explained your position well enough, please feel free to close the issue.

@stevengj
Copy link
Member

@GregPlowman, we definitely want consistency of return types; type stability is essential for performance, and we've put some effort into this for sum (see #6069, #6116, #7013, #8092, and other issues for discussions about this).

However, in your example above, the return type is consistent: it is always MyType (assuming you have defined Base.zero and Base.+ in the obvious way for MyType). Whether it returns a copy is not a type issue per se.

@GregPlowman
Copy link
Contributor Author

However, in your example above, the return type is consistent: it is always MyType (assuming you have defined Base.zero and Base.+ in the obvious way for MyType ).

Agreed. But I think we'd be no worse off if we extended this to include non-obvious definitions of + as well.

Whether it returns a copy is not a type issue per se.

Agreed. But maybe it could be seen as a non-type-related issue instead.

Firstly, some context:
I guess I'm looking for consistency and predictability (of types AND ref/copy) where possible, where it does not conflict with other more important objectives. I'm learning that type stability affects performance, so type stability is probably a high priority goal. However, there might also be other considerations, admittedly less important but none the less valid, like consistency of whether a copy or reference is returned.
When deciding between two alternatives, all other things being equal, why wouldn't you choose the more consistent and predictable? What is there to lose? Maybe we gain something.

My view of sum() is that it is a convenient library function that I can I reach for easily. Of course I can always write my own function if it doesn't suit my current purpose. That's what's good about Julia. But I would think that a library function should, where possible, be general, consistent, predictable, efficient.

Intuitively one might expect sum() to implement +() over a collection. Maybe this could even be considered a contract or an interface. It could be defined to always return the type of +(). But further, it could be defined to return a copy/ref according to how +() is defined.
In my example, I define:

type MyType
    x::Int
end
+(a::MyType, b::MyType) = MyType(a.x + b.x)
zero(::MyType) = MyType(0)

So is it unreasonable to expect sum() to return a new object here, because I defined +() that way?
If I defined +() to return a reference, or sometimes a reference and sometimes a copy, then sum() should follow. Why can't sum() honour the "contract" of implementing +() over a collection?

I could of course:

  • define my own sum() function - not as convenient as using the library function
  • use copy(sum(A)) - inefficient for most cases where n <> 1
  • length(A) == 1 ? copy(sum(A)) : sum(A) - not quite as clean
  • inherit from Number, in which case x + zero() is returned - my custom type is not really a Number in all respects, but it does highlight another inconsistency with sum()

And this is after first diagnosing the potential pitfall, then tracking it down to the inconsistency in sum(), and then coding around it.

I guess you might think that ref/copy is not of "functional or mathematical" concern because it's not about types. You might say its not relevant what is returned. In practice, however these are real programming concerns.

I like the help for reshape(), because it explicitly tells me not to rely on a copy. Similarly for fill().

reshape(A, dims)
Create an array with the same data as the given array, but with different dimensions.
An implementation for a particular type of array may choose whether the data is copied or shared.

Presumably these help tips are provided because it is relevant to users, and to provide clarity and prevent any gotchas.
For sum(), similar documentation could be provided, but it seems easy to guarantee consistency instead, without sacrificing anything.

On the type stability part, I agree that under "usual" definitions of +() and zero(), sum() is type stable. However, in the unusual case where +() returns a different type, then it will be type-unstable. I concede this is probably rare, but it just seems more correct to return the type of +(), and again what do we lose? An example for Numbers in #6069 cites +(::Int8, ::Int8) returns Int. Theoretically a user-defined type might not be closed under +() either.

@oscardssmith
Copy link
Member

Closing since we can't change this now.

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

6 participants