-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
[Skylark] Make range function lazy. #5240
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for improved iteration performance it also makes sense to implement a specialized iterator to make computation of the next element cheaper and avoid unnecessary checks that AbstractList.Itr does
This is exciting! |
for (int pos : sliceIndices) { | ||
builder.add(this.get(pos)); | ||
} | ||
return MutableList.createImmutable(builder.build()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe implement it lazily?
https://github.com/google/skylark/blob/master/library.go#L888
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
absolutely, I meant to leave a TODO for this though, since I didn't want to complicate this change even more. Also, I believe that all slice implementations should be made lazy and more efficient, since currently MutableList and Tuple both create news collections, which can be avoided. I'll try to create a new abstraction for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A TODO is fine for me.
Be careful with lazy slices, e.g.
a = [1, 2, 3]
b = a[1:]
a[2] = 5
b # [2, 3]
@@ -486,6 +486,7 @@ public void testRange() throws Exception { | |||
.testStatement("str(range(5, 0, -1))", "[5, 4, 3, 2, 1]") | |||
.testStatement("str(range(5, 0, -10))", "[5]") | |||
.testStatement("str(range(0, -3, -2))", "[0, -2]") | |||
.testStatement("str(range(3)[1:2])", "[1]") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add more tests combining range and slicing
https://github.com/google/skylark/blob/master/testdata/builtins.sky#L94
Thanks a lot! It looks good. I'll be away a few days, deferring to Jon for the approval |
added TODO and tests, thanks for the pointers @laurentlb! |
e869eec
to
be1f5db
Compare
Thanks for the PR. I see a couple issues. The first is that we should try to preserve backward compatibility until we're ready to introduce a behavioral change in a controlled way (i.e. with an --incompatible_* change flag). For range(), the current behavior is that it returns a list, but in the future we want it to return a view. The difference might be observed when the user does any of the following on the result of range():
I've added a quick test to check some of these (33f08e7). In any case, ideally they should all be maintained across any semantics-preserving optimization PR like this one. (We have an old unimplemented internal design doc on the desired view-like behavior of range(). We really should publish that and revisit the design so we all agree on what the end state will be. Created #5264 to track.) The second issue is the class hierarchy of non-list-non-tuple sequences. The pattern we're trying to evolve toward is that each distinct user-visible Skylark type corresponds to a class with a In this PR, given the backward compatibility constraints that range() objects should (for now) appear to be lists, this means that ideally RangeList and MutableList should inherit from a common interface (ListLike?) that is a SkylarkList, and the "list" To make progress, I suggest we make sure we have test coverage for range-list-compatibility on the behaviors I listed above (we might already be good, I haven't checked in detail), and then modify this PR to comply. |
In fact, I'll do that now. |
Done in 585418d. |
Thanks for such a detailed information, @brandjon! I'll rebase and see if any of the tests you've kindly added are broken. I'll also happily remove the |
after taking another look at the code, I think that maybe it would be better to just introduce the |
I updated the change to use |
Looks good to me, thanks! |
} | ||
// simplify handling of a case like range(-2) or its equivalent range(-2, 0) | ||
// by turning it into range(0, 0) | ||
stop = Math.max(start, stop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? Shouldn't this be ok albeit degenerate?
If you do this for step > 0, you probably also want to set stop = Math.min(start, stop) when step < 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after playing with Python interpreter I realized that Python actually allows this and displays the range the way it was constructed, so this change is not good. I'm going to remove it.
} | ||
} | ||
|
||
private static int computeSize(int start, int stop, int step) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're abs()
ing both the difference and step, this'll report non-zero size when the range should actually be empty due to step having the wrong sign.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've decided to implement it similarly to https://github.com/python/cpython/blob/09bb918a61031377d720f1a0fa1fe53c962791b6/Objects/rangeobject.c#L144-L210 to make it easier to port CPython behavior
final int length = Math.abs(stop - start); | ||
final int absolute_step = Math.abs(step); | ||
// round up (length / absolute_step) without using floats | ||
return 1 + ((length - 1) / absolute_step); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If length
is 0 but the absolute step is bigger than 1, the quotient will 0 and the returned result will be 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI usually we try not to modify incompatible change flags too much, so if you want to make getSlice return a range as well, I'd either do it before we flip this flag to true or else use a second flag. |
@brandjon, yeah, I just started working on it. I hope to get working slices in the next day or two if my oncall rotation is not too crazy :) |
I forgot, when you add lazy slicing, can you please also document the incompatible change flag? It goes in site/docs/skylark/backward-compatibility.md. |
Will do, thanks for the pointers @brandjon! I've also updated PR to include lazy range slices. |
added a section to backwards compatibility doc. |
RangeListView.Slice slice = RangeListView.Slice.from(size(), start, end, step, loc); | ||
int substep = slice.step * this.step; | ||
int substart = get(slice.start); | ||
int substop = get(slice.stop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really confusing which indices are inclusive and exclusive in the new code. A comment or two might help. The simplest interpretation is that start
and stop
are consistently inclusive and exclusive, throughout. In that case, slice.stop
is not necessarily a valid input to get()
, when the slice's endpoint is the same as the original range's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent point! :) I've updated code to use [start, stop) convention everywhere
@Override | ||
public Integer get(int index) { | ||
int value = start + step * index; | ||
if ((step > 0 && value > stop) || (step < 0 && value < stop)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If stop is exclusive, shouldn't it be if value >= stop
and value <= stop
?
Then again, why do the computation on values at all? Why not just test if index < 0 || index >= size
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great point! I didn't realize that index that is passed here is already normalized
* Python version: https://github.com/python/cpython/blob/09bb918a61031377d720f1a0fa1fe53c962791b6/Objects/rangeobject.c#L144 | ||
*/ | ||
private static int computeSize(int start, int stop, int step) { | ||
int low; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In light of the inclusion/exclusion confusion elsewhere, can you add a comment indicating that this is a half-open interval and it doesn't matter which var represents the included endpoint?
throw new EvalException(loc, "slice step cannot be zero"); | ||
} | ||
|
||
int upper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment that one endpoint is inclusive and the other exclusive.
int lower; | ||
if (step < 0) { | ||
lower = -1; | ||
upper = length + lower; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
length - 1
is clearer than length - lower
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to be as close to CPython implementation as possible, but in this particular case I think the usage of + length
is somewhat accidental and only hurts readability, so applied your suggestion.
25c6201
to
5d14097
Compare
thanks for the awesome suggestions, @brandjon! I've addressed all of them and added a couple of test cases. Please take another look. |
@brandjon, could you please take another look? |
Sorry for the delay - @brandjon is on leave. Thanks a lot for your contribution, it's very exciting! |
thank you, @laurentlb! I'll be happy to address any concerns Jon may have once he's back :) |
range used to use MutableList which would eagerly allocate an array list with all range elements, which is not efficient for very large ranges or when only a small number of its elements are used. This implementation uses a constant amount of RAM and computes a value for each requested index.
@@ -486,6 +486,7 @@ public void testRange() throws Exception { | |||
.testStatement("str(range(5, 0, -1))", "[5, 4, 3, 2, 1]") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test for mult, which is also now disabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought I had one... I guess I was wrong... :)
+ "range(10)[::2] # range(0, 10, 2)\n" | ||
+ "range(10)[3:0:-1] # range(3, 0, -1)</pre>" | ||
+ "Ranges are immutable, as in Python 3.") | ||
public static final class RangeList extends SkylarkList<Integer> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a preference for these new classes be in separate files. Please :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I just wasn't sure what's the convention, since the rest of SkylarkList implementations are inlined :)
private static class RangeListView extends AbstractList<Integer> { | ||
|
||
/** Iterator for increasing sequences. */ | ||
private static class IncreasingIterator extends UnmodifiableIterator<Integer> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In principle I thought it was fine that we were separating IncreasingIterator and DecreasingIterator for clarity purposes, but then it turns out that these classes are identical except for hasNext().
Perhaps we should combine them, and hasNext() can be return (step > 0) ? : cursor < stop : cursor > stop;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's how I started, but then started prematurely optimizing to avoid extra branches :)
} | ||
} | ||
|
||
public MutableList<Integer> toMutableList(Environment env) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadoc? (at least to describe the ramifications of doing this)javadoc
return MutableList.copyOf(env, contents); | ||
} | ||
|
||
public static RangeList of(int start, int stop, int step) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(apologies, meant to request changes on my past review. Marking that for posterity)
820a9b2
to
c598d2a
Compare
addressed all concerns, @c-parsons, please take another look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple very minor comments -- this is looking good!
/** Provides access to range elements based on their index. */ | ||
private static class RangeListView extends AbstractList<Integer> { | ||
|
||
/** Iterator for increasing sequences. */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix javadoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doh
} | ||
|
||
/** | ||
* @return A materialized version of the range that can be used as a <pre>list</pre> type.<p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Throughout bazel we put @return at the end of javadoc blocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
addressed all of the comments above, please take another look, @c-parsons :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks for your work!
Thank you for the review, @c-parsons! |
range used to use MutableList which would eagerly allocate an array list with all range elements, which is not efficient for very large ranges or when only a small number of its elements are used. This implementation uses a constant amount of RAM and computes a value for each requested index. For the following Skylark snippet: ``` def check_content(t): if t == []: return t return False def modulo(n): return n % 797 N = 10000000 [check_content(i) for i in range(N)] [check_content(i) for i in range(N)] [modulo(i) for i in range(N)] [modulo(i) for i in range(N)] ``` the total runtime goes from ``` $ time bazel-bin/src/main/java/com/google/devtools/skylark/Skylark test.bzl bazel-bin/src/main/java/com/google/devtools/skylark/Skylark test.bzl 93.09s user 1.67s system 316% cpu 29.930 total ``` to ``` $ time bazel-bin/src/main/java/com/google/devtools/skylark/Skylark test.bzl bazel-bin/src/main/java/com/google/devtools/skylark/Skylark test.bzl 31.45s user 0.86s system 179% cpu 17.974 total ``` which reflects the reduced system time (fewer allocations) and performance. Closes bazelbuild#5240. PiperOrigin-RevId: 204918577
range used to use MutableList which would eagerly allocate an array list with
all range elements, which is not efficient for very large ranges or when only
a small number of its elements are used.
This implementation uses a constant amount of RAM and computes a value for
each requested index.
For the following Skylark snippet:
the total runtime goes from
to
which reflects the reduced system time (fewer allocations) and performance.