-
Hi there!
This results in
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Here's a way to find out: compile a function and look at its overloaded types. >>> import awkward as ak
>>> import numba as nb
>>> @nb.njit
... def do_nothing(builder, array):
... pass
...
>>> do_nothing.overloads.keys()
odict_keys([]) (Nothing yet because we haven't compiled anything yet.) >>> do_nothing(ak.ArrayBuilder(), ak.Array([[1, 2, 3], [], [4, 5]]))
>>> do_nothing.overloads.keys()
odict_keys([
(ak.ArrayBuilderType(None), # <---- the ArrayBuilder is opaque
ak.ArrayView( # <---- the Array is specialized
ak.ListArrayType(
array(int64, 1d, C),
ak.NumpyArrayType(array(int64, 1d, A), none, {}),
none, {}),
None, ())
)
]) Calling it once compiles the function for the given argument types, and what you see above are the Numba types for the ArrayBuilder and the Array. The ArrayBuilder type is simple: it's opaque to Numba so that it can be filled with any type of data, determined at runtime. The Array type is specialized for the data that it contains so that it can generate faster code to iterate over inputs. You can also access these types from the objects: >>> ak.ArrayBuilder().numba_type
ak.ArrayBuilderType(None)
>>> ak.Array([[1, 2, 3], [], [4, 5]]).numba_type
ak.ArrayView(
ak.ListArrayType(
array(int64, 1d, C),
ak.NumpyArrayType(array(int64, 1d, A), none, {}),
none, {}),
None, ()) The ArrayBuilder's Numba type is defined here, and its only parameter (showing up as That said, knowing the types of ArrayBuilder and Array doesn't help you make anything faster. As shown above, Numba compiles the function the first time it is called, when the full types of its arguments are known. Performing this compilation a millisecond earlier when the function is defined doesn't help anything. Maybe if you're defining the function nested within some other context so it gets redefined (and therefore recompiled) repeatedly, try to put it at global scope so that it gets defined exactly once per Python process. Numba also has If it's runtime you're thinking about, there are things you can do. The first is to try to build the output without ArrayBuilder, if possible. ArrayBuilder is untyped—like Python, it has to discover the types of objects at runtime—and therefore it's slower compiled code than compiled code can be when its type is known. Right now, @ianna is working on a TypedArrayBuilder, which is more constrained in needing to know its type before being filled, and therefore should provide performance advantages. For the time being, building an Awkward Array out of In [1]: import awkward as ak
...: import numpy as np
...: import numba as nb
In [2]: @nb.njit
...: def build_with_ArrayBuilder(builder, num_lists, ave_length):
...: for i in range(num_lists):
...: builder.begin_list()
...: count = np.random.poisson(ave_length)
...: for j in range(count):
...: builder.real(np.random.normal(0, 1))
...: builder.end_list()
...: return builder
...:
In [3]: %%timeit
...:
...: array = build_with_ArrayBuilder(ak.ArrayBuilder(), 10000000, 10).snapshot()
...:
...:
8.09 s ± 11.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) and here's an example of building it manually, by constructing the same structure out of In [4]: @nb.njit
...: def build_manually(num_lists, ave_length):
...: offsets = np.empty(num_lists + 1, np.int64)
...: offsets[0] = 0
...: for i in range(num_lists):
...: count = np.random.poisson(ave_length)
...: offsets[i + 1] = offsets[i] + count
...: content = np.empty(offsets[-1], np.float64)
...: for i in range(num_lists):
...: start = offsets[i]
...: stop = offsets[i + 1]
...: for j in range(start, stop):
...: content[j] = np.random.normal(0, 1)
...: return offsets, content
...:
In [5]: %%timeit
...:
...: offsets, content = build_manually(10000000, 10)
...: array = ak.Array(
...: ak.layout.ListOffsetArray64(
...: ak.layout.Index64(offsets),
...: ak.layout.NumpyArray(content),
...: ),
...: )
...:
...:
3.88 s ± 12.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) It's about a factor of 2 in this case, though the use of random number algorithms (which adds to both cases) might be significant. As it turns out, the answer is yes: computing the random numbers accounts for nearly all of the time in the second case: In [8]: @nb.njit
...: def just_random_numbers(num_lists, ave_length):
...: out1 = 0 # to make sure LLVM doesn't compile them away
...: out2 = 0.0
...: for i in range(num_lists):
...: out1 = np.random.poisson(ave_length)
...: for j in range(out1):
...: out2 = np.random.normal(0, 1)
...: return out1, out2
...:
In [9]: %%timeit
...:
...: just_random_numbers(10000000, 10)
...:
...:
3.42 s ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) so really, building manually would be much faster than the ArrayBuilder if not for the random numbers. If the other parts of your program are significantly faster than generating random numbers, you can gain a lot from manually building the output, rather than using ArrayBuilder. If not, then don't optimize this when something else is your bottleneck. By the way, I'm giving a tutorial on Numba tomorrow: https://github.com/jpivarski-talks/2021-02-03-pyhep-numba-tutorial |
Beta Was this translation helpful? Give feedback.
Here's a way to find out: compile a function and look at its overloaded types.
(Nothing yet because we haven't compiled anything yet.)