Repository: scikit-hep/awkward

Issue:
Performance of ufuncs
Raised by @tamasgal in https://github.com/scikit-hep/uproot4/issues/90#issuecomment-689459702:

> Another problem, which I will post in a future issue that working with these arrays is far from the numpy performance (doing things like `arr > 0.5` takes ~2ms for 100 entries, while in numpy/Julia/C it should be more around a few hundred ns), but that's another story.

Original dataset: http://131.188.167.67:8889/doubly_jagged.root

```python
import uproot4
import awkward1 as ak
trks = uproot4.open("uproot-issue-90.root:E/Evt/trks")
array = trks["trks.rec_stages"].array()
ak.to_parquet(array, "awkward-issue-442.parquet")
array
# <Array [[[1, 2, 5, 3, 5, 4], ... 1], [1], [1]]] type='145028 * var * var * int64'>
```

As Parquet (faster to download and read into Awkward): https://drive.google.com/file/d/1JbiFaBaouH_amUxvGnsSHegYAQjRTJ8u/view?usp=sharing
As Pickle (larger, but retains structure: Parquet adds option-type, which complicates the performance analysis): https://drive.google.com/file/d/1KnYebahkvLK29ZggISGROHxjcpCUdO0H/view?usp=sharing

The basic idea of performance in Awkward Array is that we don't worry about the constant-time metadata manipulation but should worry about the linear-time scaling. In particular, computing `array > 3` of a doubly jagged array is pure Python: it unwraps the doubly jagged structure and calls NumPy's own `np.greater` on the inner flat content.

Because of the constant-time unwrapping, it shouldn't be surprising that the Awkward version doesn't start scaling until the array is at least 1000 entries or so. What _is_ surprising is that the linear scaling for Awkward doesn't line up with the linear scaling for NumPy, because in theory, _it isn't doing anything other than calling NumPy_.

![quick-plot](https://user-images.githubusercontent.com/1852447/92609164-e2e85e80-f27b-11ea-869c-25ac6177d292.png)

So this is a quandary.


Discussion:
You were faster a little bit! Here is a bit more detailed issue: https://github.com/scikit-hep/awkward-1.0/issues/443
Looking at your issue, I don't see how they're related. They might be related. I need to do a quick profile of this because there's a lot of linear time here that isn't due to NumPy, and I can't imagine what it might be.
Yes you are right, my issue is more related to the overhead when dealing with tinier bits of data.

Resolving patch:
diff --git a/src/awkward1/_util.py b/src/awkward1/_util.py
--- a/src/awkward1/_util.py
+++ b/src/awkward1/_util.py
@@ -486,6 +486,50 @@ def checklength(inputs):
                     + exception_suffix(__file__)
                 )
 
+    def all_same_offsets(nplike, inputs):
+        offsets = None
+        for x in inputs:
+            if isinstance(x, (
+                awkward1.layout.ListOffsetArray32,
+                awkward1.layout.ListOffsetArrayU32,
+                awkward1.layout.ListOffsetArray64,
+            )):
+                if offsets is None:
+                    offsets = nplike.asarray(x.offsets)
+                elif not nplike.array_equal(offsets, nplike.asarray(x.offsets)):
+                    return False
+            elif isinstance(x, (
+                awkward1.layout.ListArray32,
+                awkward1.layout.ListArrayU32,
+                awkward1.layout.ListArray64,
+            )):
+                starts = nplike.asarray(x.starts)
+                stops = nplike.asarray(x.stops)
+                if not nplike.array_equal(starts[1:], stops[:-1]):
+                    return False
+                if offsets is None:
+                    offsets = nplike.empty(len(starts) + 1, dtype=starts.dtype)
+                    if len(offsets) == 1:
+                        offsets[0] = 0
+                    else:
+                        offsets[:-1] = starts
+                        offsets[-1] = stops[-1]
+                elif (
+                    not nplike.array_equal(offsets[:-1], starts) or
+                    offsets[-1] != stops[-1]
+                ):
+                    return False
+            elif isinstance(x, awkward1.layout.RegularArray):
+                my_offsets = nplike.arange(0, len(x.content), x.size)
+                if offsets is None:
+                    offsets = my_offsets
+                elif not nplike.array_equal(offsets, my_offsets):
+                    return False
+            elif isinstance(x, awkward1.layout.Content):
+                return False
+        else:
+            return True
+
     def apply(inputs, depth):
         nplike = awkward1.nplike.of(*inputs)
 
@@ -698,17 +742,19 @@ def apply(inputs, depth):
 
                 outcontent = apply(nextinputs, depth + 1)
                 assert isinstance(outcontent, tuple)
+
                 return tuple(
                     awkward1.layout.RegularArray(x, maxsize) for x in outcontent
                 )
 
-            else:
+            elif not all_same_offsets(nplike, inputs):
                 fcns = [
                     custom_broadcast(x, behavior)
                     if isinstance(x, awkward1.layout.Content)
                     else None
                     for x in inputs
                 ]
+
                 first, secondround = None, False
                 for x, fcn in zip(inputs, fcns):
                     if (
@@ -718,6 +764,7 @@ def apply(inputs, depth):
                     ):
                         first = x
                         break
+
                 if first is None:
                     secondround = True
                     for x in inputs:
@@ -747,10 +794,70 @@ def apply(inputs, depth):
 
                 outcontent = apply(nextinputs, depth + 1)
                 assert isinstance(outcontent, tuple)
+
                 return tuple(
                     awkward1.layout.ListOffsetArray64(offsets, x) for x in outcontent
                 )
 
+            else:
+                lencontent, offsets, starts, stops = None, None, None, None
+                nextinputs = []
+
+                for x in inputs:
+                    if isinstance(x, (
+                        awkward1.layout.ListOffsetArray32,
+                        awkward1.layout.ListOffsetArrayU32,
+                        awkward1.layout.ListOffsetArray64,
+                    )):
+                        offsets = x.offsets
+                        lencontent = offsets[-1]
+                        nextinputs.append(x.content[:lencontent])
+
+                    elif isinstance(x, (
+                        awkward1.layout.ListArray32,
+                        awkward1.layout.ListArrayU32,
+                        awkward1.layout.ListArray64,
+                    )):
+                        starts, stops = x.starts, x.stops
+                        lencontent = nplike.max(stops)
+                        nextinputs.append(x.content[:lencontent])
+
+                    else:
+                        nextinputs.append(x)
+
+                outcontent = apply(nextinputs, depth + 1)
+
+                if isinstance(offsets, awkward1.layout.Index32):
+                    return tuple(
+                        awkward1.layout.ListOffsetArray32(offsets, x) for x in outcontent
+                    )
+                elif isinstance(offsets, awkward1.layout.IndexU32):
+                    return tuple(
+                        awkward1.layout.ListOffsetArrayU32(offsets, x) for x in outcontent
+                    )
+                elif isinstance(offsets, awkward1.layout.Index64):
+                    return tuple(
+                        awkward1.layout.ListOffsetArray64(offsets, x) for x in outcontent
+                    )
+                elif isinstance(starts, awkward1.layout.Index32):
+                    return tuple(
+                        awkward1.layout.ListArray32(starts, stops, x) for x in outcontent
+                    )
+                elif isinstance(starts, awkward1.layout.IndexU32):
+                    return tuple(
+                        awkward1.layout.ListArrayU32(starts, stops, x) for x in outcontent
+                    )
+                elif isinstance(starts, awkward1.layout.Index64):
+                    return tuple(
+                        awkward1.layout.ListArray64(starts, stops, x) for x in outcontent
+                    )
+                else:
+                    raise AssertionError(
+                        "unexpected offsets, starts: {0} {1}".format(
+                            type(offsets), type(starts)
+                        ) + exception_suffix(__file__)
+                    )
+
         elif any(isinstance(x, recordtypes) for x in inputs):
             keys = None
             length = None
@@ -1443,12 +1550,12 @@ def forever(iterable):
 
 
 class MappingProxy(MutableMapping):
-    """A type suitable for use with layout.ArrayCache
-    
+    """
+    A type suitable for use with layout.ArrayCache.
+
     This can be used to wrap plain dict instances if need be,
     since they are not able to be weak referenced.
     """
-
     def __init__(self, base):
         self.base = base
 


Test patch: