[POC] PERF: 1d version of the cython grouped aggregation algorithms
For now, this is just a small example to open discussion about how to tackle this topic (not meant for merging).

Context: for a columnar dataframe (with ArrayManager), we would also perform the groupby operations column by column (instead of on the 2D blocks). Most of our custom cython aggregation algorithms are currently written for 2D arrays (eg `group_add`, `group_mean`, etc. I think only the fill/shift/any/all are 1D). But by being 2D, that means they have some performance penalty when using it for a single column.

Experiment: I copied the implementation for "mean" and made a 1D version of it (exactly the same code, but removing one `for` loop over the columns, and simplifying the indices to index into 1D arrays instead of 2D arrays). 
And with that did some timings to see the impact:

```python
N = 1_000_000
df = pd.DataFrame(np.random.randn(N, 10), columns=list("abcdefghij"))
df["key"] = np.random.randint(0, 100, size=N)

# below I am manually doing the aggregation part for one column of the following groupby operation
expected = df.groupby("key").mean()
```

Using the existing 2D algorithm on a single column:

```python
codes, uniques = pd.factorize(df["key"], sort=True)
out_shape = (len(uniques), 1)
out_dtype = "float64"

values = df[['a']]._mgr.blocks[0].values.T

result = np.empty(out_shape, out_dtype)
result.fill(np.nan)
counts = np.zeros(len(uniques), dtype=np.int64)

pd._libs.groupby.group_mean_float64(result, counts, values, codes, min_count=-1)
```

Using the new 1D version:

```python
out_shape1d = (len(uniques),)
out_dtype = "float64"

values1d = df['a']._mgr.blocks[0].values.copy()

result1d = np.empty(out_shape1d, out_dtype)
result1d.fill(np.nan)
counts = np.zeros(len(uniques), dtype=np.int64)

pd._libs.groupby.group_mean_1d_float64(result1d, counts, values1d, codes, min_count=-1)
```

Ensure that both give the same result:

```
In [4]: np.allclose(result.squeeze(), expected['a'].values)
Out[4]: True

In [5]: np.allclose(result1d, expected['a'].values)
Out[5]: True
```

And timings for both:

```
In [8]: %%timeit
   ...: result = np.empty(out_shape, out_dtype)
   ...: result.fill(np.nan)
   ...: counts = np.zeros(len(uniques), dtype=np.int64)
   ...: pd._libs.groupby.group_mean_float64(result, counts, values, codes, min_count=-1)

4.15 ms ± 54.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [9]: %%timeit
   ...: result1d = np.empty(out_shape1d, out_dtype)
   ...: result1d.fill(np.nan)
   ...: counts = np.zeros(len(uniques), dtype=np.int64)
   ...: pd._libs.groupby.group_mean_1d_float64(result1d, counts, values1d, codes, min_count=-1)

2.62 ms ± 38.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```

So for a single column, the 1D version is ca 30-40% faster (increasing the size of the array with 10x, I also get 40% faster).

Some notes:

- The actual aggregation is only a part of the full groupby operation. But for sufficiently large data, it becomes the most significant portion. E.g. for the `df.groupby("key").mean()` example above, the `group_mean` takes ca 1.5x the time of the factorization step (that's for multiple columns, though, when profiling it for grouping a single column, the ratio becomes the other way around: ca 38% in group_mean and ca 62% in factorization)
- If we want to have an optimized groupby for the ArrayManager-backed dataframe, I think we will need those 1D versions of the cython algos. Of course, what I currently did here would give a huge duplication in code (and also not easily templateable or so) on the short term.

xref https://github.com/pandas-dev/pandas/issues/39146