PERF: further improve take (reindex/unstack) for ArrayManager
State: closed

A few related changes to `take_1d` / `ArrayManager.unstack`:

- Check in `ArrayManager.unstack` whether there will be missing values introduced or not, and if not, pass `allow_fill=False` to the `take` function, so this can take a more performant path (and doesn't need to check repeatedly for `indexer == -1`). And the same for `ArrayManager.reindex_indexer`.
- In the `take_1d`, if `allow_fill=False`, use the numpy `take` instead of our own cython implementation (numpy is a bit faster).
-  Move the `ensure_int64` check for the indexer out of the shared `_take_preprocess_indexer_and_fill_value`. This way, we can do this once in ArrayManager.unstack and avoid doing it repeatedly for the same indexer inside `take_1d`.

Case from the `frame_methods.py::Unstack.time_full_product` ASV benchmark:

```python
dtype = 'int'
m = 100
n = 1000

levels = np.arange(m)
index = pd.MultiIndex.from_product([levels] * 2)
columns = np.arange(n)
values = np.arange(m * m * n).reshape(m * m, n)

df = pd.DataFrame(values, index, columns)
df_am = df._as_manager("array")
```

with the combined changes, this gives:

```
In [2]: %timeit df_am.unstack()
1.09 s ± 68.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)  <-- master
276 ms ± 19.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)  <-- PR
```

Labels:
name: Performance
description: Memory or execution speed performance
name: Reshaping
description: Concat, Merge/Join, Stack/Unstack, Explode
name: Algos
description: Non-arithmetic algos: value_counts, factorize, sorting, isin, clip, shift, diff

Comments:
Are there further comments here?
yeah i think generally we should be using intp for indexing as we
do this for numpy 

we should change our cython routines to do this (this maybe a non trivial ask)

it is likely this is a perf bottleneck (as we are moving between intp and int64 a fair amount)

i would be wrong though and this is not really an issue
checks are failing (maybe just rebase needed)
> it is likely this is a perf bottleneck (as we are moving between intp and int64 a fair amount)

arent these aliases on 64bit builds?
Can/should we just have take_nd do an earlier check for ndim == 1 and then dispatch to take_1d?

In NDArrayBackedExtensionArray.take if i change take_nd to

```
        if self.ndim == 1 and axis == 0 and allow_fill:
            indices = ensure_platform_int(indices)
            validate_indices(indices, len(self))
            new_data = take_1d(
                self._ndarray, indices, allow_fill=allow_fill, fill_value=fill_value
            )

        else:
             new_data = [what we do right now]
```

then on the time_full_product benchmark referenced https://github.com/pandas-dev/pandas/issues/39146#issuecomment-792817698 we shave off 25-30% relative to master (non-AM)
> Can/should we just have take_nd do an earlier check for ndim == 1 and then dispatch to take_1d?

That might be possible (although the check for EA and `np.asarray` might get a bit in the way for that). But we would still have the separate take_1d implemention, so I personally prefer to keep using that for explicitness in those places where we know it's only about 1D arrays (like the places in ArrayManager where I am using it now).
> so I personally prefer to keep using that for explicitness in those places where we know it's only about 1D arrays (like the places in ArrayManager where I am using it now).

im fine with that, just suggesting if we have a clear optimization for the general `take` lets use it
> im fine with that, just suggesting if we have a clear optimization for the general take lets use it

Certainly, see https://github.com/pandas-dev/pandas/pull/40405
For the rest, any remaining comments before merging this?
ill take another look this afternoon
lgtm @jbrockmendel merge when ready
LGTM.  Merging this evening absent other comments.
thanks @jorisvandenbossche