-
Notifications
You must be signed in to change notification settings - Fork 266
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
Faster groupby! #179
Faster groupby! #179
Conversation
Issue pytoolz#178 impressed upon me just how costly attribute resolution can be. In this case, `groupby` was made faster by avoiding resolving the attribute `list.append`. This implementation is also more memory efficient than the current version that uses a `defaultdict` that gets cast to a `dict`. While casting a defaultdict `d` to a dict as `dict(d)` is fast, it is still a fast *copy*. Honorable mention goes to the following implementation: ```python def groupby_alt(func, seq): d = collections.defaultdict(lambda: [].append) for item in seq: d[func(item)](item) rv = {} for k, v in iteritems(d): rv[k] = v.__self__ return rv ``` This alternative implementation can at times be *very* impressive. You should play with it!
d[func(item)].append(item) | ||
return dict(d) | ||
key = func(item) | ||
if key not in d: |
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.
Using try-except here might be a bit faster in cases with more than a few repeats. I get only modest improvements on my tiny benchmark though.
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.
The groupby_alt
that I posted in the above always performs better than using a try-except
here. You should really test groupby_alt
too!
I get only modest improvements on my tiny benchmark though.
Yeah, the improvements can indeed be modest, but they are most significant when many items are the same. Also, the new implementation is always better than the old implementation for my benchmarks, including all the same, all different, and tiny seq
.
This is really cool stuff. I'm a bit concerned that we've completely left understandable Python, but for operations like groupby I think it's clearly worth it. Caching method dispatch is also a nice trick up our sleeve. Is this class of optimizations useful at all in Cython? |
A few people I run into know that |
Ha! Well, you laid down the gauntlet a couple times in blog posts and messages saying something to the effect "we believe this is the fastest pure Python solution available." Although not nearly as clear or pythonic, a faster solution was found. On guard!
Yeah, agreed. Although this is clearly out of the ordinary--and some might say perverse--I don't think it's so obtuse that it can't be understood with a bit of effort. Perhaps I should add a couple code comments to make it easier to understand (i.e., this is faster because it avoids
I have no idea. I really want to have a variational benchmarking framework before exploring such optimizations. |
Maybe we should retain the idiomatic implementation as a comment |
Good idea. We also do this for |
Benchmarks have a tendency to use data that is pathologically perfect. In such pathological cases, it was unclear whether the current or previous implementation would be preferred. However, when the input data gets shuffled, the implementation in this commit is *clearly* superior. Therefore, I believe this is the best implementation for *real* data.
By the way, @mrocklin, I made a slight tweak to the algorithm in |
Hrm, one largeish benchmark would be to group up part of the web graph. Here is a 500MB chunk http://data.dws.informatik.uni-mannheim.de/hyperlinkgraph/network/part-r-00253.gz . Or use the following to get the whole thing (warning, a few hundred GB compressed).
|
Wow, the 500MB chunk is more like 4GB! EDIT: There is a typo in Before I play with that, I suppose I should share my current benchmarks. Let's start with defining the functions: import collections
import random
from toolz import identity
from toolz.compatibility import iteritems
def groupby_orig(func, seq):
d = collections.defaultdict(list)
for item in seq:
d[func(item)].append(item)
return dict(d)
def groupby_alt(func, seq):
d = collections.defaultdict(lambda: [].append)
for item in seq:
d[func(item)](item)
rv = {}
for k, v in iteritems(d):
rv[k] = v.__self__
return rv
def groupby_prev(func, seq):
d = {}
for item in seq:
key = func(item)
if key not in d:
d[key] = [item].append
else:
d[key](item)
for k, v in iteritems(d):
d[k] = v.__self__
return d
def groupby_cur(func, seq):
d = {}
for item in seq:
key = func(item)
if key in d:
d[key](item)
else:
d[key] = [].append # XXX typo!
# d[key] = [item].append <-- should be this
for k, v in iteritems(d):
d[k] = v.__self__
return d Now lets use the perfectly behaved data. This covers all data is duplicated, all data is unique, and many states in-between: In [2]: data = range(10000) * 1
In [3]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 12.3 ms per loop
In [4]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 16 ms per loop
In [5]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 10.5 ms per loop
In [6]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 9.68 ms per loop
In [7]: data = range(10000/3) * 3
In [8]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 7.44 ms per loop
In [9]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 6.82 ms per loop
In [10]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 6.98 ms per loop
In [11]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 6.68 ms per loop
In [12]: data = range(1000) * 10
In [13]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 6.2 ms per loop
In [14]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 4.89 ms per loop
In [15]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 5.62 ms per loop
In [16]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 5.58 ms per loop
In [17]: data = range(100) * 100
In [18]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.49 ms per loop
In [19]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.89 ms per loop
In [20]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.8 ms per loop
In [21]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.9 ms per loop
In [22]: data = range(10) * 1000
In [23]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.31 ms per loop
In [24]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.56 ms per loop
In [25]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.61 ms per loop
In [26]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.67 ms per loop
In [27]: data = range(1) * 10000
In [28]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.12 ms per loop
In [29]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.4 ms per loop
In [30]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.47 ms per loop
In [31]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.52 ms per loop I see some small variation in the results when re-running these in different IPython sessions. The posted results are pretty typical, but I would say that Now I'm going to intentionally add variance to the results by shuffling the data: In [32]: data = range(10000) * 1
In [33]: random.shuffle(data)
In [34]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 15.3 ms per loop
In [35]: %timeit groupby_alt(identity, data)
10 loops, best of 3: 19.4 ms per loop
In [36]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 13.1 ms per loop
In [37]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 11 ms per loop
In [38]: data = range(10000/3) * 3
In [39]: random.shuffle(data)
In [40]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 8.11 ms per loop
In [41]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 7.67 ms per loop
In [42]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.92 ms per loop
In [43]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 7.43 ms per loop
In [44]: data = range(1000) * 10
In [45]: random.shuffle(data)
In [46]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 6.56 ms per loop
In [47]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 5.41 ms per loop
In [48]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 6.11 ms per loop
In [49]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 6.07 ms per loop
In [50]: data = range(100) * 100
In [51]: random.shuffle(data)
In [52]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.61 ms per loop
In [53]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.99 ms per loop
In [54]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.87 ms per loop
In [55]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 5 ms per loop
In [56]: data = range(10) * 1000
In [57]: random.shuffle(data)
In [58]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.17 ms per loop
In [59]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.6 ms per loop
In [60]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.67 ms per loop
In [61]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.64 ms per loop
In [62]: data = range(1) * 10000
In [63]: random.shuffle(data)
In [64]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.21 ms per loop
In [65]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 3.5 ms per loop
In [66]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.32 ms per loop
In [67]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.53 ms per loop
In [68]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 4.59 ms per loop It's pretty obvious that Let's look at a completely arbitrary and artificial data set to show that performance in all regimes is important: In [69]: data = range(2000) + range(2000, 3000) * 5 + range(3000, 3100) * 40
In [70]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 8.3 ms per loop
In [71]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 7.67 ms per loop
In [72]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.24 ms per loop
In [73]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 7.08 ms per loop
In [74]: random.shuffle(data)
In [75]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 9.1 ms per loop
In [76]: %timeit groupby_alt(identity, data)
100 loops, best of 3: 8.57 ms per loop
In [77]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 8.07 ms per loop
In [78]: %timeit groupby_cur(identity, data)
100 loops, best of 3: 7.64 ms per loop This is why I prefer |
I'm getting really good results with a new implementation: def groupby_new(func, seq):
rv = {}
d = {}
for item in seq:
key = func(item)
if key in d:
d[key](item)
elif key not in rv:
rv[key] = [item]
else:
val = d[key] = rv[key].append
val(item)
return rv This performs great for groups that have a lot of items, great for groups that have a single item, and good enough for the worst case scenario when groups have two or three items each. |
What is the intuition behind this approach? It isn't immediately obvious to me (and I haven't had the time yet to sit down and actually see what's going on.) |
To get a sense of performance behavior and implementation rationale, it helps to compare against the version that was previously used in def groupby_new(func, seq):
rv = {}
d = {}
Even though we use two dicts, the impact on memory usage is minimal. In the worst case scenario in which all groups have two items, the memory footprint of the containers--the dicts and lists, but not their contents--increases by about 25%. for item in seq:
key = func(item) Standard iteration. if key in d:
d[key](item) This is for optimal asymptotic performance as the groups get larger. Note that this avoids an attribute look-up; i.e., it doesn't do elif key not in rv:
rv[key] = [item] This allows for fast initialization of groups. The implementation that was previously in else:
val = d[key] = rv[key].append
val(item) We are adding a second item to a group and adding return rv No casting or post-filtering is necessary. Up next I'll share benchmarks. |
Benchmarks! Sorry again for the long wall of numbers: import collections
import random
from toolz.compatibility import iteritems
def groupby_orig(func, seq):
""" Implementation currently in ``toolz``"""
d = collections.defaultdict(list)
for item in seq:
d[func(item)].append(item)
return dict(d)
def groupby_old(func, seq):
""" Modified version of what was previously in ``toolz``"""
d = {}
for item in seq:
key = func(item)
if key not in d:
d[key] = [item]
else:
d[key].append(item)
return d
def groupby_prev(func, seq):
""" First version from this PR"""
d = {}
for item in seq:
key = func(item)
if key not in d:
d[key] = [item].append
else:
d[key](item)
for k, v in iteritems(d):
d[k] = v.__self__
return d
def groupby_new(func, seq):
""" Newest version in this PR (not yet pushed)"""
rv = {}
d = {}
for item in seq:
key = func(item)
if key in d:
d[key](item)
elif key not in rv:
rv[key] = [item]
else:
val = d[key] = rv[key].append
val(item)
return rv In [2]: identity = lambda x: x
In [3]: data = range(10000) * 1
In [4]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 11.6 ms per loop
In [5]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.55 ms per loop
In [6]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 10.7 ms per loop
In [7]: %timeit groupby_new(identity, data)
100 loops, best of 3: 7.62 ms per loop
In [8]: data = range(10000/3) * 3
In [9]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 7.23 ms per loop
In [10]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.87 ms per loop
In [11]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.09 ms per loop
In [12]: %timeit groupby_new(identity, data)
100 loops, best of 3: 7.23 ms per loop
In [13]: data = range(1000) * 10
In [14]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 6.17 ms per loop
In [15]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.79 ms per loop
In [16]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 5.91 ms per loop
In [17]: %timeit groupby_new(identity, data)
100 loops, best of 3: 5.88 ms per loop
In [18]: data = range(100) * 100
In [19]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.46 ms per loop
In [20]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.49 ms per loop
In [21]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.98 ms per loop
In [22]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.88 ms per loop
In [23]: data = range(10) * 1000
In [24]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.21 ms per loop
In [25]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.06 ms per loop
In [26]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.85 ms per loop
In [27]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.55 ms per loop
In [28]: data = range(1) * 10000
In [29]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.27 ms per loop
In [30]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.11 ms per loop
In [31]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.5 ms per loop
In [32]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.56 ms per loop and the same benchmarks as above but with In [33]: data = range(10000) * 1
In [34]: random.shuffle(data)
In [35]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 13.8 ms per loop
In [36]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.78 ms per loop
In [37]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 13.4 ms per loop
In [38]: %timeit groupby_new(identity, data)
100 loops, best of 3: 8.87 ms per loop
In [39]: data = range(10000/3) * 3
In [40]: random.shuffle(data)
In [41]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 7.77 ms per loop
In [42]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.48 ms per loop
In [43]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.89 ms per loop
In [44]: %timeit groupby_new(identity, data)
100 loops, best of 3: 8.3 ms per loop
In [45]: data = range(1000) * 10
In [46]: random.shuffle(data)
In [47]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 6.53 ms per loop
In [48]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.13 ms per loop
In [49]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 6.18 ms per loop
In [50]: %timeit groupby_new(identity, data)
100 loops, best of 3: 6.28 ms per loop
In [51]: data = range(100) * 100
In [52]: random.shuffle(data)
In [53]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.49 ms per loop
In [54]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.37 ms per loop
In [55]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.94 ms per loop
In [56]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.95 ms per loop
In [57]: data = range(10) * 1000
In [58]: random.shuffle(data)
In [59]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.25 ms per loop
In [60]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.09 ms per loop
In [61]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.77 ms per loop
In [62]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.6 ms per loop
In [63]: data = range(1) * 10000
In [64]: random.shuffle(data)
In [65]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 5.17 ms per loop
In [66]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.03 ms per loop
In [67]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 4.55 ms per loop
In [68]: %timeit groupby_new(identity, data)
100 loops, best of 3: 4.61 ms per loop Next is the benchmark I used in a previous post to test a few regimes of behavior in the same data set: In [69]: data = range(2000) + range(2000, 3000) * 5 + range(3000, 3100) * 40
In [70]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 8.07 ms per loop
In [71]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.2 ms per loop
In [72]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.21 ms per loop
In [73]: %timeit groupby_new(identity, data)
100 loops, best of 3: 6.74 ms per loop
In [74]: random.shuffle(data)
In [75]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 8.65 ms per loop
In [76]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.86 ms per loop
In [77]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 7.96 ms per loop
In [78]: %timeit groupby_new(identity, data)
100 loops, best of 3: 7.49 ms per loop We expect In [79]: data = range(3000) + range(3000, 4500) * 2 + range(4500, 5500) * 3
In [80]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 8.77 ms per loop
In [81]: %timeit groupby_old(identity, data)
100 loops, best of 3: 6.46 ms per loop
In [82]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 8.35 ms per loop
In [83]: %timeit groupby_new(identity, data)
100 loops, best of 3: 7.5 ms per loop
In [84]: random.shuffle(data)
In [85]: %timeit groupby_orig(identity, data)
100 loops, best of 3: 9.32 ms per loop
In [86]: %timeit groupby_old(identity, data)
100 loops, best of 3: 7.23 ms per loop
In [87]: %timeit groupby_prev(identity, data)
100 loops, best of 3: 9.35 ms per loop
In [88]: %timeit groupby_new(identity, data)
100 loops, best of 3: 8.46 ms per loop As expected in the last test Even though these benchmarks are artificial, subject to systematic bias, and probably don't accurately reflect your data set (or mine or his or ...), I think they do tell a consistent story. I know it's not a complete story, but the result I get from it are that |
The previous commit was wrong. It is slower than the commit before. There was a mistake in the code used for benchmarks. I believe the new implementation has optimal performance as groups get larger. It is also fast when creating a new group. It is slowest when each group has two or three items in it, but it is still fast enough so as to not impact the general performance of the algorithm. A note on size: using a second dict in the implementation doesn't add much memory. Let us consider the size used by all of the containers--dicts and lists--but not their contents. For the worst case scenario in which both dicts have two items (note that `fastdict` only has groups of length two or greater), memory usage is only increased by about 25% by having a second dict.
I'm pretty much First, when 10% or more of the elements being grouped form groups of length one, then def groupby_new(func, seq):
""" Newest version in this PR"""
rv = {}
fastdict = {}
for item in seq:
key = func(item)
if key in fastdict:
d[key](item)
elif key not in rv:
rv[key] = [item]
else:
val = fastdict[key] = rv[key].append
val(item)
return rv Second, when the average group size is about five or greater--and virtually no groups are of size one--then def groupby_alt(func, seq):
d = collections.defaultdict(lambda: [].append)
for item in seq:
d[func(item)](item)
rv = {}
for k, v in iteritems(d):
rv[k] = v.__self__
return rv Third, in-between the aforementioned regimes, the competition between variants is close, and other versions can become optimal for a short segment of "data space." It is my opinion that To put into context, the original version from this PR, def groupby_prev(func, seq):
""" First version from this PR"""
d = {}
for item in seq:
key = func(item)
if key not in d:
d[key] = [item].append
else:
d[key](item)
for k, v in iteritems(d):
d[k] = v.__self__
return d @mrocklin, do you have a preference? How long are the tails in your data? Do you want to optimize |
Oh, let me share how I arrived at the 10% rule of thumb.
Also, |
Sounds then like we should make a decision and merge this. I don't have a good understanding on the distribution of datasets that will be used with
If you want me to make an arbitrary decision I'm happy to do so. I definitely trust your intuition here more than mine. Probably we want something that's somewhat robust. Another thought is to put a couple implementations into Let me know if you want me to make an arbitrary decision. |
Yes, please do :) But, let me share my current bias first. I am leaning slightly towards
An argument for Previous revisions of
Not sure about this. I have six implementations of @mrocklin, decision time! |
All things being equal, reason 3 compels me. Lets go with I like the idea of the groupby implementations living in a separate repo that we can refer to. |
This is `groupby_alt` from the original commit comment in this branch. See discussion at pytoolz#179. This version performs very well as groups become larger. The previous implementation performs well when 10% or more of the elements form groups of length one. We plan to have various implementations in `benchmarkz` repository, which will let contributors add benchmarks that they care about and easily run them on all variants of `groupby`.
Done. I thought that might be the deciding factor :) |
I think this is the longest discussion to lines-changed ratio PR I've ever seen. Merging in a bit if no comments. |
Indeed! There were some interesting discoveries and discussions though. I'm eager to test the variations of +1 to merge. |
Issue #178 impressed upon me just how costly attribute resolution can be. In this case,
groupby
was made faster by avoiding resolving the attributelist.append
.This implementation is also more memory efficient than the current version that uses a
defaultdict
that gets cast to adict
. While casting a defaultdictd
to a dict asdict(d)
is fast, it is still a fast copy.Honorable mention goes to the following implementation:
This alternative implementation can at times be very impressive. You should play with it!