Repository: modin-project/modin

Issue:
[PERF] Slow sort_values in value_counts
New `sort_values` implementation called inside of `value_counts` is very slow in comparison with the previous implementation.

**Script to reproduce**
```python
import modin.pandas as pd
import time
import ray

ray.init()

s = pd.read_csv("slow_sort_values.csv", index_col=0).squeeze(axis=1)
t1 = time.time()
s = s.value_counts()
print(f"value_counts: {time.time() - t1}")
```

**Time**
```bash
# time with the new `sort_values` implementation
grp = 0.15747547149658203
sort = 7.6473588943481445
value_counts: 7.805760622024536

# time with the previous `sort_values` implementation
grp = 0.16362214088439941
sort = 0.17642974853515625
value_counts: 0.34135985374450684
```

`grp` and `sort` are simple time wrappers over [groupby](https://github.com/modin-project/modin/blob/8b40350f77662ee9edd0837adec8a7388257577b/modin/pandas/base.py#L3475) and [sort_values](https://github.com/modin-project/modin/blob/8b40350f77662ee9edd0837adec8a7388257577b/modin/pandas/base.py#L3477) calls in `modin/pandas/base.py`.

**Modin version: latest master**
**Python version: Python 3.8.15**
**Num Cpus on Machine: 112**
**Data: [slow_sort_values.csv](https://github.com/modin-project/modin/files/10425524/slow_sort_values.csv)**




Discussion:
@RehanSD, can you take a look?
It seems that we're dealing with excessive parallelism here. The reason for that is that [`.shuffle_partitions`](https://github.com/modin-project/modin/blob/209a03c04215e9fc79359aa47fc41b8986676bc2/modin/core/dataframe/pandas/partitioning/partition_manager.py#L1524) now operates on a `Ncores ^ 2` amount of partitions. 

The implementation creates a `len(df._partitions) == Ncores` number of quantile-based splits for each row partition (which are also `Ncores` of them) thus storing `Ncores ^ 2` partitions inside `split_row_partitions`:

https://github.com/modin-project/modin/blob/209a03c04215e9fc79359aa47fc41b8986676bc2/modin/core/dataframe/pandas/partitioning/partition_manager.py#L1562-L1582

There are two slow places:
- the splitting itself (it takes time for Ray to store NCores amount of NCores length tuples in its plasma store)
- the kernel execution itself, as it's being applied to `NCores ^ 2` partitions

So summarizing this, it seems that the shuffling implementation is not suitable enough for small-sized data (like the one we have in the reproducer). The perf degradation is also visible on [slightly bigger data in ASV](https://modin.org/modin-bench/#benchmarks.TimeSortValues.time_sort_values?p-shape=%5B5000%2C%205000%5D&p-shape=%5B1000000%2C%2010%5D&p-columns_number=1&p-columns_number=2&p-ascending_list=False&p-ascending_list=True&machine=xeon-e5):

<details><summary>Graph from ASV under the spoiler</summary>

![image](https://user-images.githubusercontent.com/62142979/217851121-34636c97-db3c-4e13-8408-cc9c68e84bbe.png)

(click on the picture to zoom in)

</details>

For now, I'll try to reduce the number of overall partitions in this implementation by reducing the number of quantile-based splits depending on the data shape and combining these splits into a single column partition to also slightly reduce the number of resulting Ray remotes.
We should re-measure this case after a lot of improvements to the reshuffling mechanism were done. However, I still think that for such small amount of data (as far as I understand there are around 10_000 rows in the reproducer) the reshuffling implementation of sorting will still be quite expensive and the most beneficial would be to proceed with an old full-column implementation. We should carefully consider some heuristic that would dispatch between these two implementations depending on the number of rows (or we may not need to introduce any heuristic at all if we fix #6384 first)

Resolving patch:
diff --git a/modin/core/dataframe/pandas/partitioning/partition_manager.py b/modin/core/dataframe/pandas/partitioning/partition_manager.py
--- a/modin/core/dataframe/pandas/partitioning/partition_manager.py
+++ b/modin/core/dataframe/pandas/partitioning/partition_manager.py
@@ -257,8 +257,16 @@ def groupby_reduce(
             )
         else:
             mapped_partitions = cls.map_partitions(partitions, map_func)
+
+        # Assuming, that the output will not be larger than the input,
+        # keep the current number of partitions.
+        num_splits = min(len(partitions), NPartitions.get())
         return cls.map_axis_partitions(
-            axis, mapped_partitions, reduce_func, enumerate_partitions=True
+            axis,
+            mapped_partitions,
+            reduce_func,
+            enumerate_partitions=True,
+            num_splits=num_splits,
         )
 
     @classmethod


Test patch: