Repository: modin-project/modin
Pull request: FIX-#3686: optimize `__getitem__` flow of loc/iloc
Signed-off-by: Dmitry Chigarev <dmitry.chigarev@intel.com>

<!--
Thank you for your contribution!
Please review the contributing docs: https://modin.readthedocs.io/en/latest/CONTRIBUTING.html
if you have questions about contributing.
-->

## What do these changes do?

<!-- Please give a short brief about these changes. -->

- [x] commit message follows format outlined [here](https://modin.readthedocs.io/en/latest/contributing.html)
- [x] passes `flake8 modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] passes `black --check modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] signed commit with `git commit -s` <!-- you can amend your commit with a signature via `git commit -amend -s` -->
- [x] Resolves #3686 <!-- issue must be created for each patch -->
- [x] tests added and passing
- [x] module layout described at `docs/developer/architecture.rst` is up-to-date <!-- if you have added, renamed or removed files or directories please update the documentation accordingly -->

Results of ASV benchmarks (existed ones were combined with the added in #3700):

<details><summary>PandasOnRay execution</summary>

Launch command:
```
MODIN_CPUS=16 MODIN_TEST_DATASET_SIZE=Big asv continuous master issue-3686 -b "^benchmarks.TimeIndexing" --show-stderr --no-only-changed
```

Note: the regression in the case of "bool_python_list" is caused by the need of converting a python boolean mask to a numpy array (it wasn't done before).
```
       before           after         ratio
     [8d67cd3a]       [4acbef88]
     <issue-3566~2>       <issue-3686>
+         307±2ms          352±5ms     1.15  benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_python_list')
         19.4±1ms         21.7±2ms    ~1.11  benchmarks.TimeIndexingColumns.time___getitem__([1000000, 10])
       17.9±0.5ms         18.9±1ms     1.05  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'python_list_take_10_values')
          141±6ms          144±6ms     1.02  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'bool_array')
         20.1±1ms         20.2±1ms     1.01  benchmarks.TimeIndexingColumns.time_loc([1000000, 10])
          308±3ms          310±2ms     1.01  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_series')
          248±6ms         248±10ms     1.00  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'function')
      1.17±0.01ms      1.17±0.02ms     1.00  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'continuous_slice')
       26.5±0.3ms         26.2±1ms     0.99  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'scalar')
          149±7ms          147±5ms     0.99  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'slice')
          383±4ms          378±5ms     0.99  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_python_list')
       29.6±0.6ms         29.2±1ms     0.99  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'continuous_slice')
       12.9±0.3ms       12.6±0.4ms     0.98  benchmarks.TimeIndexingColumns.time_loc([5000, 5000])
          643±1ms          628±8ms     0.98  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'python_list_take_all_values')
       30.8±0.5ms         29.7±1ms     0.97  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'python_list_take_all_values')
       31.0±0.9ms         29.9±2ms     0.97  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'numpy_array_take_all_values')
       12.6±0.2ms       12.1±0.2ms     0.96  benchmarks.TimeIndexingColumns.time___getitem__([5000, 5000])
        163±0.6ms        155±0.7ms     0.95  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'slice')
         21.2±2ms       20.0±0.5ms     0.94  benchmarks.TimeIndexingColumns.time_iloc([1000000, 10])
         175±10ms          165±8ms     0.94  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'bool_series')
          152±5ms          143±5ms     0.94  benchmarks.TimeIndexing.time_iloc([5000, 5000], 'bool_python_list')
          152±7ms          140±8ms     0.93  benchmarks.TimeIndexing.time_loc([5000, 5000], 'slice')
       13.4±0.4ms       12.2±0.7ms     0.91  benchmarks.TimeIndexingColumns.time_iloc([5000, 5000])
-         614±8ms          552±5ms     0.90  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'numpy_array_take_all_values')
-         331±6ms        294±0.9ms     0.89  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_array')
-      10.5±0.2ms       9.00±0.3ms     0.86  benchmarks.TimeIndexing.time_loc([1000000, 10], 'scalar')
          162±4ms          136±4ms    ~0.84  benchmarks.TimeIndexing.time_loc([5000, 5000], 'bool_array')
-         349±7ms          292±2ms     0.84  benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_array')
-        840±20ms         698±10ms     0.83  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'function')
-      8.87±0.2ms       7.12±0.2ms     0.80  benchmarks.TimeIndexing.time_loc([1000000, 10], 'python_list_take_10_values')
          169±6ms          133±6ms    ~0.79  benchmarks.TimeIndexing.time_loc([5000, 5000], 'bool_python_list')
-         215±8ms         167±10ms     0.78  benchmarks.TimeIndexing.time_loc([5000, 5000], 'bool_series')
-         936±4ms         695±30ms     0.74  benchmarks.TimeIndexing.time_loc([1000000, 10], 'function')
-        361±20ms         248±10ms     0.69  benchmarks.TimeIndexing.time_loc([5000, 5000], 'function')
-         846±8ms          569±8ms     0.67  benchmarks.TimeIndexing.time_loc([1000000, 10], 'numpy_array_take_all_values')
-      47.7±0.5ms       30.0±0.6ms     0.63  benchmarks.TimeIndexing.time_loc([5000, 5000], 'numpy_array_take_all_values')
-        42.7±2ms         26.4±1ms     0.62  benchmarks.TimeIndexing.time_loc([5000, 5000], 'scalar')
-      50.2±0.3ms       31.0±0.6ms     0.62  benchmarks.TimeIndexing.time_loc([5000, 5000], 'python_list_take_all_values')
-        48.7±1ms       29.3±0.6ms     0.60  benchmarks.TimeIndexing.time_loc([5000, 5000], 'continuous_slice')
-         519±5ms          309±2ms     0.60  benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_series')
-      1.11±0.01s          629±7ms     0.57  benchmarks.TimeIndexing.time_loc([1000000, 10], 'python_list_take_all_values')
-      16.6±0.4ms      8.60±0.08ms     0.52  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'scalar')
-      14.7±0.5ms       6.57±0.2ms     0.45  benchmarks.TimeIndexing.time_iloc([1000000, 10], 'python_list_take_10_values')
-        37.3±1ms         15.4±2ms     0.41  benchmarks.TimeIndexing.time_loc([5000, 5000], 'python_list_take_10_values')
-         455±8ms          154±3ms     0.34  benchmarks.TimeIndexing.time_loc([1000000, 10], 'slice')
-         438±8ms       6.04±0.2ms     0.01  benchmarks.TimeIndexing.time_loc([1000000, 10], 'continuous_slice')
```

</details>

<details><summary>OmnisciOnNative execution</summary>

Launch command:
```
MODIN_ENGINE="Native" MODIN_STORAGE_FORMAT="Omnisci" MODIN_TEST_DATASET_SIZE=Big asv continuous master issue-3686 -b "omnisci.benchmarks.TimeIndexing" --show-stderr --no-only-changed
```

```
       before           after         ratio
     [8d67cd3a]       [4acbef88]
     <issue-3566~2>       <issue-3686>
+        447±10μs        593±100μs    ~1.33  omnisci.benchmarks.TimeIndexingColumns.time_loc([1000000, 10])
          403±7μs         405±10μs     1.00  omnisci.benchmarks.TimeIndexingColumns.time___getitem__([1000000, 10])
          157±6μs          159±1μs     1.01  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'continuous_slice')
       6.49±0.4ms       6.13±0.2ms     0.95  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'scalar')
          115±6ms          105±3ms     0.92  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_series')
         41.9±7ms        33.1±10ms    ~0.79  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'slice')
-         148±4ms          113±4ms     0.76  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_python_list')
-        603±20μs         436±20μs     0.72  omnisci.benchmarks.TimeIndexingColumns.time_iloc([1000000, 10])
-        182±10ms         129±10ms     0.71  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'python_list_take_all_values')
       13.8±0.7ms         6.67±5ms    ~0.48  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'scalar')
         79.9±5ms         27.4±8ms    ~0.34  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'bool_array')
-        80.4±8ms         24.9±4ms     0.31  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'function')
-     1.27±0.04ms          386±5μs     0.30  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'python_list_take_10_values')
-        162±10ms        46.8±20ms     0.29  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'numpy_array_take_all_values')
-         345±3ms        94.7±20ms     0.27  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'numpy_array_take_all_values')
-         358±3ms         96.4±4ms     0.27  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_python_list')
-         611±2ms         152±10ms     0.25  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'python_list_take_all_values')
-         186±2ms         37.2±9ms     0.20  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'function')
-         187±1ms         29.1±8ms     0.16  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'slice')
-        888±20ms          119±4ms     0.13  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_series')
-         401±2ms         24.1±4ms     0.06  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'bool_array')
-      5.86±0.3ms         241±30μs     0.04  omnisci.benchmarks.TimeIndexing.time_iloc([1000000, 10], 'python_list_take_10_values')
-         185±1ms          177±3μs     0.00  omnisci.benchmarks.TimeIndexing.time_loc([1000000, 10], 'continuous_slice')
```

</details>

Problem statement:
Optimize `__getitem__` flow of `loc/iloc`
Indexing DataFrames using `iloc` and `loc` appears to take a lot of time. Here is the results of our ASV Benchmarks ran on OmniSci backend:
```
DataFrame.loc	[30000000, 10]_function	3.37
DataFrame.loc	[30000000, 10]_list	11.38
DataFrame.loc	[30000000, 10]_slice	3.44
DataFrame.loc	[30000000, 10]_bool	11.47
DataFrame.loc	[30000000, 10]_scalar	0.01

DataFrame.iloc	[30000000, 10]_function	2.09
DataFrame.iloc	[30000000, 10]_list	4.17
DataFrame.iloc	[30000000, 10]_slice	1.05
DataFrame.iloc	[30000000, 10]_bool	3.31
DataFrame.iloc	[30000000, 10]_scalar	0.24
```

Profiling shows that most of the time spent on the front-end, inside `loc/iloc` locators logic:
```
asv profile --output=$PROFILING_DIR "omnisci.benchmarks.TimeIndexing.time_loc(.*)" --python=same

ORDER OF CALLS   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1  	    1    0.000    0.000    7.598    7.598 indexing.py:519(__getitem__) <---- df.loc[key], takes 7.5 seconds
      -           ...
      5           7/6    0.000    0.000    0.999    0.166 dataframe.py:1612(_execute)  <---- actual execution takes only 1 second
      6           4/2    0.000    0.000    0.999    0.499 dataframe.py:1703(_execute_arrow)
      7             1    0.000    0.000    0.998    0.998 dataframe.py:1758(_arrow_row_slice)
      8             1    0.000    0.000    0.998    0.998 {method 'take' of 'pyarrow.lib.Table' objects}
      2             1    0.000    0.000    0.001    0.001 query_compiler.py:83(method_wrapper)
      3             1    0.000    0.000    0.001    0.001 query_compiler.py:272(view)
      4             1    0.000    0.000    0.001    0.001 dataframe.py:270(mask)
```

It's required to optimize indexing logic on the front-end so it wouldn't be a bottleneck

Hints from issue discussion:

Commit message: Disable broken 'BaseOnPython' tests

Signed-off-by: Dmitry Chigarev <dmitry.chigarev@intel.com>

Patch to review:
diff --git a/modin/core/dataframe/pandas/dataframe/dataframe.py b/modin/core/dataframe/pandas/dataframe/dataframe.py
index 37a482aed07..375759ecb96 100644
--- a/modin/core/dataframe/pandas/dataframe/dataframe.py
+++ b/modin/core/dataframe/pandas/dataframe/dataframe.py
@@ -954,7 +954,12 @@ def _get_dict_of_block_index(self, axis, indices):
         if isinstance(indices, slice) or (is_range_like(indices) and indices.step == 1):
             # Converting range-like indexer to slice
             indices = slice(indices.start, indices.stop, indices.step)
-            if indices == slice(None) or indices == slice(0, None):
+            # Detecting full-axis grab
+            if (
+                indices.start in (None, 0)
+                and indices.step in (None, 1)
+                and (indices.stop is None or indices.stop >= len(self.axes[axis]))
+            ):
                 return OrderedDict(
                     zip(
                         range(self._partitions.shape[axis]),
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
index 218eb9675b5..f92a1a66cdc 100644
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -32,9 +32,10 @@
 import numpy as np
 import pandas
 from pandas.api.types import is_list_like, is_bool
-from pandas.core.dtypes.common import is_integer
+from pandas.core.dtypes.common import is_integer, is_bool_dtype, is_integer_dtype
 from pandas.core.indexing import IndexingError
 from modin.error_message import ErrorMessage
+from itertools import compress
 
 from .dataframe import DataFrame
 from .series import Series
@@ -126,9 +127,34 @@ def is_boolean_array(x):
     bool
         True if argument is an array of bool, False otherwise.
     """
+    if isinstance(x, (np.ndarray, Series, pandas.Series, pandas.Index)):
+        return is_bool_dtype(x.dtype)
+    elif isinstance(x, (DataFrame, pandas.DataFrame)):
+        return all(map(is_bool_dtype, x.dtypes))
     return is_list_like(x) and all(map(is_bool, x))
 
 
+def is_integer_array(x):
+    """
+    Check that argument is an array of integers.
+
+    Parameters
+    ----------
+    x : object
+        Object to check.
+
+    Returns
+    -------
+    bool
+        True if argument is an array of integers, False otherwise.
+    """
+    if isinstance(x, (np.ndarray, Series, pandas.Series, pandas.Index)):
+        return is_integer_dtype(x.dtype)
+    elif isinstance(x, (DataFrame, pandas.DataFrame)):
+        return all(map(is_integer_dtype, x.dtypes))
+    return is_list_like(x) and all(map(is_integer, x))
+
+
 def is_integer_slice(x):
     """
     Check that argument is an array of int.
@@ -175,6 +201,32 @@ def is_range_like(obj):
     )
 
 
+def boolean_mask_to_numeric(indexer):
+    """
+    Convert boolean mask to numeric indices.
+
+    Parameters
+    ----------
+    indexer : list-like of booleans
+
+    Returns
+    -------
+    np.ndarray of ints
+        Numerical positions of ``True`` elements in the passed `indexer`.
+    """
+    if isinstance(indexer, (np.ndarray, Series, pandas.Series)):
+        return np.where(indexer)[0]
+    else:
+        # It's faster to build the resulting numpy array from the reduced amount of data via
+        # `compress` iterator than convert non-numpy-like `indexer` to numpy and apply `np.where`.
+        return np.fromiter(
+            # `itertools.compress` masks `data` with the `selectors` mask,
+            # works about ~10% faster than a pure list comprehension
+            compress(data=range(len(indexer)), selectors=indexer),
+            dtype=np.int64,
+        )
+
+
 _ILOC_INT_ONLY_ERROR = """
 Location based indexing can only have [integer, integer slice (START point is
 INCLUDED, END point is EXCLUDED), listlike of integers, boolean array] types.
@@ -505,6 +557,43 @@ def _parse_row_and_column_locators(self, tup):
         col_loc = col_loc(self.df) if callable(col_loc) else col_loc
         return row_loc, col_loc, _compute_ndim(row_loc, col_loc)
 
+    # HACK: This method bypasses regular ``loc/iloc.__getitem__`` flow in order to ensure better
+    # performance in the case of boolean masking. The only purpose of this method is to compensate
+    # for a lack of backend's indexing API, there is no Query Compiler method allowing masking
+    # along both axis when any of the indexers is a boolean. That's why rows and columns masking
+    # phases are separate in this case.
+    # TODO: Remove this method and handle this case naturally via ``loc/iloc.__getitem__`` flow
+    # when QC API would support both-axis masking with boolean indexers.
+    def _handle_boolean_masking(self, row_loc, col_loc):
+        """
+        Retrieve dataset according to the boolean mask for rows and an indexer for columns.
+
+        In comparison with the regular ``loc/iloc.__getitem__`` flow this method efficiently
+        masks rows with a Modin Series boolean mask without materializing it (if the selected
+        execution implements such masking).
+
+        Parameters
+        ----------
+        row_loc : modin.pandas.Series of bool dtype
+            Boolean mask to index rows with.
+        col_loc : object
+            An indexer along column axis.
+
+        Returns
+        -------
+        modin.pandas.DataFrame or modin.pandas.Series
+            Located dataset.
+        """
+        ErrorMessage.catch_bugs_and_request_email(
+            failure_condition=not isinstance(row_loc, Series),
+            extra_log=f"Only ``modin.pandas.Series`` boolean masks are acceptable, got: {type(row_loc)}",
+        )
+        masked_df = self.df.__constructor__(
+            query_compiler=self.qc.getitem_array(row_loc._query_compiler)
+        )
+        # Passing `slice(None)` as a row indexer since we've just applied it
+        return type(self)(masked_df)[(slice(None), col_loc)]
+
 
 class _LocIndexer(_LocationIndexerBase):
     """
@@ -539,24 +628,11 @@ def __getitem__(self, key):
         row_loc, col_loc, ndim = self._parse_row_and_column_locators(key)
         self.row_scalar = is_scalar(row_loc)
         self.col_scalar = is_scalar(col_loc)
-        if isinstance(row_loc, slice) and row_loc == slice(None):
-            # If we're only slicing columns, handle the case with `__getitem__`
-            if not isinstance(col_loc, slice):
-                # Boolean indexers can just be sliced into the columns object and
-                # then passed to `__getitem__`
-                if is_boolean_array(col_loc):
-                    col_loc = self.df.columns[col_loc]
-                return self.df.__getitem__(col_loc)
-            else:
-                result_slice = self.df.columns.slice_locs(col_loc.start, col_loc.stop)
-                return self.df.iloc[:, slice(*result_slice)]
+
+        if isinstance(row_loc, Series) and is_boolean_array(row_loc):
+            return self._handle_boolean_masking(row_loc, col_loc)
 
         row_lookup, col_lookup = self._compute_lookup(row_loc, col_loc)
-        if any(i == -1 for i in row_lookup) or any(i == -1 for i in col_lookup):
-            raise KeyError(
-                "Passing list-likes to .loc or [] with any missing labels is no longer "
-                "supported, see https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#deprecate-loc-reindex-listlike"
-            )
         result = super(_LocIndexer, self).__getitem__(row_lookup, col_lookup, ndim)
         if isinstance(result, Series):
             result._parent = self.df
@@ -564,7 +640,10 @@ def __getitem__(self, key):
         col_loc_as_list = [col_loc] if self.col_scalar else col_loc
         row_loc_as_list = [row_loc] if self.row_scalar else row_loc
         # Pandas drops the levels that are in the `loc`, so we have to as well.
-        if hasattr(result, "index") and isinstance(result.index, pandas.MultiIndex):
+        if (
+            isinstance(result, (Series, DataFrame))
+            and result._query_compiler.has_multiindex()
+        ):
             if (
                 isinstance(result, Series)
                 and not isinstance(col_loc_as_list, slice)
@@ -583,7 +662,7 @@ def __getitem__(self, key):
         if (
             hasattr(result, "columns")
             and not isinstance(col_loc_as_list, slice)
-            and isinstance(result.columns, pandas.MultiIndex)
+            and result._query_compiler.has_multiindex(axis=1)
             and all(
                 col_loc_as_list[i] in result.columns.levels[i]
                 for i in range(len(col_loc_as_list))
@@ -689,49 +768,82 @@ def _compute_lookup(self, row_loc, col_loc):
 
         Returns
         -------
-        row_lookup : numpy.ndarray
+        row_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of index labels.
-        col_lookup : numpy.ndarray
+        col_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of columns labels.
-        """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
-        if is_list_like(row_loc) and len(row_loc) == 1:
-            if (
-                isinstance(self.qc.index.values[0], np.datetime64)
-                and type(row_loc[0]) != np.datetime64
-            ):
-                row_loc = [pandas.to_datetime(row_loc[0])]
 
-        if isinstance(row_loc, slice):
-            row_lookup = self.qc.index.get_indexer_for(
-                self.qc.index.to_series().loc[row_loc]
-            )
-        elif self.qc.has_multiindex():
-            if isinstance(row_loc, pandas.MultiIndex):
-                row_lookup = self.qc.index.get_indexer_for(row_loc)
-            else:
-                row_lookup = self.qc.index.get_locs(row_loc)
-        elif is_boolean_array(row_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            row_lookup = [i for i, row_val in enumerate(row_loc) if row_val]
-        else:
-            row_lookup = self.qc.index.get_indexer_for(row_loc)
-        if isinstance(col_loc, slice):
-            col_lookup = self.qc.columns.get_indexer_for(
-                self.qc.columns.to_series().loc[col_loc]
-            )
-        elif self.qc.has_multiindex(axis=1):
-            if isinstance(col_loc, pandas.MultiIndex):
-                col_lookup = self.qc.columns.get_indexer_for(col_loc)
+        Notes
+        -----
+        Usage of `slice(None)` as a resulting lookup is a hack to pass information about
+        full-axis grab without computing actual indices that triggers lazy computations.
+        Ideally, this API should get rid of using slices as indexers and either use a
+        common ``Indexer`` object or range and ``np.ndarray`` only.
+        """
+        lookups = []
+        for axis, axis_loc in enumerate((row_loc, col_loc)):
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice) or is_range_like(axis_loc):
+                if isinstance(axis_loc, slice) and axis_loc == slice(None):
+                    axis_lookup = axis_loc
+                else:
+                    axis_labels = self.qc.get_axis(axis)
+                    # `slice_indexer` returns a fully-defined numeric slice for a non-fully-defined labels-based slice
+                    axis_lookup = axis_labels.slice_indexer(
+                        axis_loc.start, axis_loc.stop, axis_loc.step
+                    )
+                    # Converting negative indices to its actual positions:
+                    axis_lookup = pandas.RangeIndex(
+                        start=(
+                            axis_lookup.start
+                            if axis_lookup.start >= 0
+                            else axis_lookup.start + len(axis_labels)
+                        ),
+                        stop=(
+                            axis_lookup.stop
+                            if axis_lookup.stop >= 0
+                            else axis_lookup.stop + len(axis_labels)
+                        ),
+                        step=axis_lookup.step,
+                    )
+            elif self.qc.has_multiindex(axis):
+                # `Index.get_locs` raises an IndexError by itself if missing labels were provided,
+                # we don't have to do missing-check for the received `axis_lookup`.
+                if isinstance(axis_loc, pandas.MultiIndex):
+                    axis_lookup = self.qc.get_axis(axis).get_indexer_for(axis_loc)
+                else:
+                    axis_lookup = self.qc.get_axis(axis).get_locs(axis_loc)
+            elif is_boolean_array(axis_loc):
+                axis_lookup = boolean_mask_to_numeric(axis_loc)
             else:
-                col_lookup = self.qc.columns.get_locs(col_loc)
-        elif is_boolean_array(col_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            col_lookup = [i for i, col_val in enumerate(col_loc) if col_val]
-        else:
-            col_lookup = self.qc.columns.get_indexer_for(col_loc)
-        return row_lookup, col_lookup
+                axis_labels = self.qc.get_axis(axis)
+                if is_list_like(axis_loc) and not isinstance(
+                    axis_loc, (np.ndarray, pandas.Index)
+                ):
+                    # `Index.get_indexer_for` works much faster with numpy arrays than with python lists,
+                    # so although we lose some time here on converting to numpy, `Index.get_indexer_for`
+                    # speedup covers the loss that we gain here.
+                    axis_loc = np.array(axis_loc, dtype=axis_labels.dtype)
+                axis_lookup = axis_labels.get_indexer_for(axis_loc)
+                # `Index.get_indexer_for` sets -1 value for missing labels, we have to verify whether
+                # there are any -1 in the received indexer to raise a KeyError here.
+                missing_mask = axis_lookup < 0
+                if missing_mask.any():
+                    missing_labels = (
+                        # Converting `axis_loc` to maskable `np.array` to not fail
+                        # on masking non-maskable list-like
+                        np.array(axis_loc)[missing_mask]
+                        if is_list_like(axis_loc)
+                        else axis_loc
+                    )
+                    raise KeyError(f"Missing labels were provided: {missing_labels}")
+
+            if isinstance(axis_lookup, pandas.Index) and not is_range_like(axis_lookup):
+                axis_lookup = axis_lookup.values
+
+            lookups.append(axis_lookup)
+        return lookups
 
 
 class _iLocIndexer(_LocationIndexerBase):
@@ -770,6 +882,9 @@ def __getitem__(self, key):
         self._check_dtypes(row_loc)
         self._check_dtypes(col_loc)
 
+        if isinstance(row_loc, Series) and is_boolean_array(row_loc):
+            return self._handle_boolean_masking(row_loc, col_loc)
+
         row_lookup, col_lookup = self._compute_lookup(row_loc, col_loc)
         result = super(_iLocIndexer, self).__getitem__(row_lookup, col_lookup, ndim)
         if isinstance(result, Series):
@@ -833,11 +948,11 @@ def _compute_lookup(self, row_loc, col_loc):
         Ideally, this API should get rid of using slices as indexers and either use a
         common ``Indexer`` object or range and ``np.ndarray`` only.
         """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
         lookups = []
         for axis, axis_loc in enumerate((row_loc, col_loc)):
-            if isinstance(axis_loc, slice) and axis_loc.step is None:
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice):
                 axis_lookup = (
                     axis_loc
                     if axis_loc == slice(None)
@@ -845,13 +960,31 @@ def _compute_lookup(self, row_loc, col_loc):
                         *axis_loc.indices(len(self.qc.get_axis(axis)))
                     )
                 )
-            else:
-                axis_lookup = (
-                    pandas.RangeIndex(len(self.qc.get_axis(axis)))
-                    .to_series()
-                    .iloc[axis_loc]
-                    .index
+            elif is_range_like(axis_loc):
+                axis_lookup = pandas.RangeIndex(
+                    axis_loc.start, axis_loc.stop, axis_loc.step
                 )
+            elif is_boolean_array(axis_loc):
+                axis_lookup = boolean_mask_to_numeric(axis_loc)
+            else:
+                if isinstance(axis_loc, pandas.Index):
+                    axis_loc = axis_loc.values
+                elif is_list_like(axis_loc) and not isinstance(axis_loc, np.ndarray):
+                    # `Index.__getitem__` works much faster with numpy arrays than with python lists,
+                    # so although we lose some time here on converting to numpy, `Index.__getitem__`
+                    # speedup covers the loss that we gain here.
+                    axis_loc = np.array(axis_loc, dtype=np.int64)
+                # Relatively fast check allows us to not trigger `self.qc.get_axis()` computation
+                # if there're no negative indices and so they're not depend on the axis length.
+                if isinstance(axis_loc, np.ndarray) and not (axis_loc < 0).any():
+                    axis_lookup = axis_loc
+                else:
+                    axis_lookup = pandas.RangeIndex(len(self.qc.get_axis(axis)))[
+                        axis_loc
+                    ]
+
+            if isinstance(axis_lookup, pandas.Index) and not is_range_like(axis_lookup):
+                axis_lookup = axis_lookup.values
             lookups.append(axis_lookup)
         return lookups
 
@@ -871,8 +1004,8 @@ def _check_dtypes(self, locator):
         """
         is_int = is_integer(locator)
         is_int_slice = is_integer_slice(locator)
-        is_int_list = is_list_like(locator) and all(map(is_integer, locator))
+        is_int_arr = is_integer_array(locator)
         is_bool_arr = is_boolean_array(locator)
 
-        if not any([is_int, is_int_slice, is_int_list, is_bool_arr]):
+        if not any([is_int, is_int_slice, is_int_arr, is_bool_arr]):
             raise ValueError(_ILOC_INT_ONLY_ERROR)
diff --git a/modin/pandas/test/dataframe/test_indexing.py b/modin/pandas/test/dataframe/test_indexing.py
index b591749606b..867123d6418 100644
--- a/modin/pandas/test/dataframe/test_indexing.py
+++ b/modin/pandas/test/dataframe/test_indexing.py
@@ -39,6 +39,7 @@
     extra_test_parameters,
 )
 from modin.config import NPartitions
+from modin.utils import get_current_execution
 
 NPartitions.put(4)
 
@@ -344,17 +345,20 @@ def test_loc(data):
     pandas_df_copy3.loc[lambda df: df[key1].isin(list(range(1000))), key1] = 42
     df_equals(modin_df_copy3, pandas_df_copy3)
 
-    # From issue #1775
-    df_equals(
-        modin_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
-        pandas_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
-    )
+    # Disabled for `BaseOnPython` because of the issue with `getitem_array`:
+    # https://github.com/modin-project/modin/issues/3701
+    if get_current_execution() != "BaseOnPython":
+        # From issue #1775
+        df_equals(
+            modin_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
+            pandas_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
+        )
 
-    # Read values, selecting rows with a callable and a column with a scalar.
-    df_equals(
-        pandas_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
-        modin_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
-    )
+        # Read values, selecting rows with a callable and a column with a scalar.
+        df_equals(
+            pandas_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
+            modin_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
+        )
 
     # From issue #1374
     with pytest.raises(KeyError):
@@ -570,6 +574,29 @@ def test_loc_series():
     df_equals(pd_df, md_df)
 
 
+@pytest.mark.parametrize("locator_name", ["loc", "iloc"])
+@pytest.mark.parametrize(
+    "slice_indexer",
+    [
+        slice(None, None, -2),
+        slice(1, 10, None),
+        slice(None, 10, None),
+        slice(10, None, None),
+        slice(10, None, -2),
+        slice(-10, None, -2),
+        slice(None, 1_000_000_000, None),
+    ],
+)
+def test_loc_iloc_slice_indexer(locator_name, slice_indexer):
+    md_df, pd_df = create_test_dfs(test_data_values[0])
+    # Shifting the index, so labels won't match its position
+    shifted_index = pandas.RangeIndex(1, len(md_df) + 1)
+    md_df.index = shifted_index
+    pd_df.index = shifted_index
+
+    eval_general(md_df, pd_df, lambda df: getattr(df, locator_name)[slice_indexer])
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_pop(request, data):
     modin_df = pd.DataFrame(data)
diff --git a/modin/pandas/test/test_groupby.py b/modin/pandas/test/test_groupby.py
index 2e116c78ca4..526662e727a 100644
--- a/modin/pandas/test/test_groupby.py
+++ b/modin/pandas/test/test_groupby.py
@@ -1216,11 +1216,16 @@ def eval_shift(modin_groupby, pandas_groupby):
         pandas_groupby,
         lambda groupby: groupby.shift(periods=-3),
     )
-    eval_general(
-        modin_groupby,
-        pandas_groupby,
-        lambda groupby: groupby.shift(axis=1, fill_value=777),
-    )
+    # Disabled for `BaseOnPython` because of the issue with `getitem_array`.
+    # groupby.shift internally masks the source frame with a Series boolean mask,
+    # doing so ends up in the `getitem_array` method, that is broken for `BaseOnPython`:
+    # https://github.com/modin-project/modin/issues/3701
+    if get_current_execution() != "BaseOnPython":
+        eval_general(
+            modin_groupby,
+            pandas_groupby,
+            lambda groupby: groupby.shift(axis=1, fill_value=777),
+        )
 
 
 def test_groupby_on_index_values_with_loop():


Code review comments:
diff_hunk: @@ -689,49 +768,82 @@ def _compute_lookup(self, row_loc, col_loc):
 
         Returns
         -------
-        row_lookup : numpy.ndarray
+        row_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of index labels.
-        col_lookup : numpy.ndarray
+        col_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of columns labels.
-        """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
-        if is_list_like(row_loc) and len(row_loc) == 1:
-            if (
-                isinstance(self.qc.index.values[0], np.datetime64)
-                and type(row_loc[0]) != np.datetime64
-            ):
-                row_loc = [pandas.to_datetime(row_loc[0])]
 
-        if isinstance(row_loc, slice):
-            row_lookup = self.qc.index.get_indexer_for(
-                self.qc.index.to_series().loc[row_loc]
-            )
-        elif self.qc.has_multiindex():
-            if isinstance(row_loc, pandas.MultiIndex):
-                row_lookup = self.qc.index.get_indexer_for(row_loc)
-            else:
-                row_lookup = self.qc.index.get_locs(row_loc)
-        elif is_boolean_array(row_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            row_lookup = [i for i, row_val in enumerate(row_loc) if row_val]
-        else:
-            row_lookup = self.qc.index.get_indexer_for(row_loc)
-        if isinstance(col_loc, slice):
-            col_lookup = self.qc.columns.get_indexer_for(
-                self.qc.columns.to_series().loc[col_loc]
-            )
-        elif self.qc.has_multiindex(axis=1):
-            if isinstance(col_loc, pandas.MultiIndex):
-                col_lookup = self.qc.columns.get_indexer_for(col_loc)
+        Notes
+        -----
+        Usage of `slice(None)` as a resulting lookup is a hack to pass information about
+        full-axis grab without computing actual indices that triggers lazy computations.
+        Ideally, this API should get rid of using slices as indexers and either use a
+        common ``Indexer`` object or range and ``np.ndarray`` only.
+        """
+        lookups = []
+        for axis, axis_loc in enumerate((row_loc, col_loc)):
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice) or is_range_like(axis_loc):
+                if isinstance(axis_loc, slice) and axis_loc == slice(None):
+                    axis_lookup = axis_loc
+                else:
+                    axis_labels = self.qc.get_axis(axis)
+                    # `slice_indexer` returns a fully-defined numeric slice for a non-fully-defined labels-based slice
+                    axis_lookup = axis_labels.slice_indexer(
+                        axis_loc.start, axis_loc.stop, axis_loc.step
+                    )
+                    # Converting negative indices to its actual positions:
path: modin/pandas/indexing.py
text: @user1:
```suggestion
                    # Converting negative indices to their actual positions:
```

diff_hunk: @@ -689,49 +768,82 @@ def _compute_lookup(self, row_loc, col_loc):
 
         Returns
         -------
-        row_lookup : numpy.ndarray
+        row_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of index labels.
-        col_lookup : numpy.ndarray
+        col_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of columns labels.
-        """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
-        if is_list_like(row_loc) and len(row_loc) == 1:
-            if (
-                isinstance(self.qc.index.values[0], np.datetime64)
-                and type(row_loc[0]) != np.datetime64
-            ):
-                row_loc = [pandas.to_datetime(row_loc[0])]
 
-        if isinstance(row_loc, slice):
-            row_lookup = self.qc.index.get_indexer_for(
-                self.qc.index.to_series().loc[row_loc]
-            )
-        elif self.qc.has_multiindex():
-            if isinstance(row_loc, pandas.MultiIndex):
-                row_lookup = self.qc.index.get_indexer_for(row_loc)
-            else:
-                row_lookup = self.qc.index.get_locs(row_loc)
-        elif is_boolean_array(row_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            row_lookup = [i for i, row_val in enumerate(row_loc) if row_val]
-        else:
-            row_lookup = self.qc.index.get_indexer_for(row_loc)
-        if isinstance(col_loc, slice):
-            col_lookup = self.qc.columns.get_indexer_for(
-                self.qc.columns.to_series().loc[col_loc]
-            )
-        elif self.qc.has_multiindex(axis=1):
-            if isinstance(col_loc, pandas.MultiIndex):
-                col_lookup = self.qc.columns.get_indexer_for(col_loc)
+        Notes
+        -----
+        Usage of `slice(None)` as a resulting lookup is a hack to pass information about
+        full-axis grab without computing actual indices that triggers lazy computations.
+        Ideally, this API should get rid of using slices as indexers and either use a
+        common ``Indexer`` object or range and ``np.ndarray`` only.
+        """
+        lookups = []
+        for axis, axis_loc in enumerate((row_loc, col_loc)):
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice) or is_range_like(axis_loc):
+                if isinstance(axis_loc, slice) and axis_loc == slice(None):
+                    axis_lookup = axis_loc
+                else:
+                    axis_labels = self.qc.get_axis(axis)
+                    # `slice_indexer` returns a fully-defined numeric slice for a non-fully-defined labels-based slice
+                    axis_lookup = axis_labels.slice_indexer(
+                        axis_loc.start, axis_loc.stop, axis_loc.step
+                    )
+                    # Converting negative indices to its actual positions:
+                    axis_lookup = pandas.RangeIndex(
+                        start=(
+                            axis_lookup.start
+                            if axis_lookup.start >= 0
+                            else axis_lookup.start + len(axis_labels)
+                        ),
+                        stop=(
+                            axis_lookup.stop
+                            if axis_lookup.stop >= 0
+                            else axis_lookup.stop + len(axis_labels)
+                        ),
+                        step=axis_lookup.step,
+                    )
+            elif self.qc.has_multiindex(axis):
+                # `Index.get_locs` raises an IndexError by itself if missing labels were provided,
+                # we don't have to do missing-check for the received `axis_lookup`.
+                if isinstance(axis_loc, pandas.MultiIndex):
+                    axis_lookup = self.qc.get_axis(axis).get_indexer_for(axis_loc)
+                else:
+                    axis_lookup = self.qc.get_axis(axis).get_locs(axis_loc)
+            elif is_boolean_array(axis_loc):
+                axis_lookup = boolean_mask_to_numeric(axis_loc)
             else:
-                col_lookup = self.qc.columns.get_locs(col_loc)
-        elif is_boolean_array(col_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            col_lookup = [i for i, col_val in enumerate(col_loc) if col_val]
-        else:
-            col_lookup = self.qc.columns.get_indexer_for(col_loc)
-        return row_lookup, col_lookup
+                axis_labels = self.qc.get_axis(axis)
+                if is_list_like(axis_loc) and not isinstance(
+                    axis_loc, (np.ndarray, pandas.Index)
+                ):
+                    # `Index.get_indexer_for` works much faster with numpy arrays than with python lists,
+                    # so although we lose some time here on converting to numpy, `Index.get_indexer_for`
+                    # speedup covers the loss that we gain here.
+                    axis_loc = np.array(axis_loc, dtype=axis_labels.dtype)
+                axis_lookup = axis_labels.get_indexer_for(axis_loc)
+                # `Index.get_indexer_for` sets -1 value for missing labels, we have to verify whether
+                # there are any -1 in the received indexer to raise a KeyError here.
+                missing_mask = axis_lookup < 0
+                if missing_mask.any():
+                    missing_labels = (
+                        # Converting `axis_loc` to maskable `np.array` to not fail
+                        # on masking non-maskable list-like
+                        np.array(axis_loc)[missing_mask]
+                        if is_list_like(axis_loc)
+                        else axis_loc
+                    )
+                    raise KeyError(f"Missing labels were provided: {missing_labels}")
path: modin/pandas/indexing.py
text: @user1:
is the error message consistent with what pandas report in this case?

@author:
No, it wasn't. In pandas, it's just a KeyError without any message. Aligned it with pandas.
```python
>>> df.loc[10]
KeyError: 10
```

diff_hunk: @@ -833,25 +948,43 @@ def _compute_lookup(self, row_loc, col_loc):
         Ideally, this API should get rid of using slices as indexers and either use a
         common ``Indexer`` object or range and ``np.ndarray`` only.
         """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
         lookups = []
         for axis, axis_loc in enumerate((row_loc, col_loc)):
-            if isinstance(axis_loc, slice) and axis_loc.step is None:
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice):
                 axis_lookup = (
                     axis_loc
                     if axis_loc == slice(None)
                     else pandas.RangeIndex(
                         *axis_loc.indices(len(self.qc.get_axis(axis)))
                     )
                 )
-            else:
-                axis_lookup = (
-                    pandas.RangeIndex(len(self.qc.get_axis(axis)))
-                    .to_series()
-                    .iloc[axis_loc]
-                    .index
+            elif is_range_like(axis_loc):
+                axis_lookup = pandas.RangeIndex(
+                    axis_loc.start, axis_loc.stop, axis_loc.step
                 )
+            elif is_boolean_array(axis_loc):
+                axis_lookup = boolean_mask_to_numeric(axis_loc)
+            else:
+                if isinstance(axis_loc, pandas.Index):
+                    axis_loc = axis_loc.values
+                elif is_list_like(axis_loc) and not isinstance(axis_loc, np.ndarray):
+                    # `Index.__getitem__` works much faster with numpy arrays than with python lists,
+                    # so although we lose some time here on converting to numpy, `Index.__getitem__`
+                    # speedup covers the loss that we gain here.
+                    axis_loc = np.array(axis_loc, dtype=np.int64)
+                # Relatively fast check allows us to not trigger `self.qc.get_axis()` computation
+                # if there're no negative indices and so they're not depend on the axis length.
path: modin/pandas/indexing.py
text: @user1:
```suggestion
                # if there're no negative indices and so they don't depend on the axis length.
```

diff_hunk: @@ -689,49 +768,82 @@ def _compute_lookup(self, row_loc, col_loc):
 
         Returns
         -------
-        row_lookup : numpy.ndarray
+        row_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of index labels.
-        col_lookup : numpy.ndarray
+        col_lookup : slice(None) if full axis grab, pandas.RangeIndex if repetition is detected, numpy.ndarray otherwise
             List of columns labels.
-        """
-        row_loc = [row_loc] if is_scalar(row_loc) else row_loc
-        col_loc = [col_loc] if is_scalar(col_loc) else col_loc
-        if is_list_like(row_loc) and len(row_loc) == 1:
-            if (
-                isinstance(self.qc.index.values[0], np.datetime64)
-                and type(row_loc[0]) != np.datetime64
-            ):
-                row_loc = [pandas.to_datetime(row_loc[0])]
 
-        if isinstance(row_loc, slice):
-            row_lookup = self.qc.index.get_indexer_for(
-                self.qc.index.to_series().loc[row_loc]
-            )
-        elif self.qc.has_multiindex():
-            if isinstance(row_loc, pandas.MultiIndex):
-                row_lookup = self.qc.index.get_indexer_for(row_loc)
-            else:
-                row_lookup = self.qc.index.get_locs(row_loc)
-        elif is_boolean_array(row_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            row_lookup = [i for i, row_val in enumerate(row_loc) if row_val]
-        else:
-            row_lookup = self.qc.index.get_indexer_for(row_loc)
-        if isinstance(col_loc, slice):
-            col_lookup = self.qc.columns.get_indexer_for(
-                self.qc.columns.to_series().loc[col_loc]
-            )
-        elif self.qc.has_multiindex(axis=1):
-            if isinstance(col_loc, pandas.MultiIndex):
-                col_lookup = self.qc.columns.get_indexer_for(col_loc)
+        Notes
+        -----
+        Usage of `slice(None)` as a resulting lookup is a hack to pass information about
+        full-axis grab without computing actual indices that triggers lazy computations.
+        Ideally, this API should get rid of using slices as indexers and either use a
+        common ``Indexer`` object or range and ``np.ndarray`` only.
+        """
+        lookups = []
+        for axis, axis_loc in enumerate((row_loc, col_loc)):
+            if is_scalar(axis_loc):
+                axis_loc = np.array([axis_loc])
+            if isinstance(axis_loc, slice) or is_range_like(axis_loc):
+                if isinstance(axis_loc, slice) and axis_loc == slice(None):
+                    axis_lookup = axis_loc
+                else:
+                    axis_labels = self.qc.get_axis(axis)
+                    # `slice_indexer` returns a fully-defined numeric slice for a non-fully-defined labels-based slice
+                    axis_lookup = axis_labels.slice_indexer(
+                        axis_loc.start, axis_loc.stop, axis_loc.step
+                    )
+                    # Converting negative indices to its actual positions:
+                    axis_lookup = pandas.RangeIndex(
+                        start=(
+                            axis_lookup.start
+                            if axis_lookup.start >= 0
+                            else axis_lookup.start + len(axis_labels)
+                        ),
+                        stop=(
+                            axis_lookup.stop
+                            if axis_lookup.stop >= 0
+                            else axis_lookup.stop + len(axis_labels)
+                        ),
+                        step=axis_lookup.step,
+                    )
+            elif self.qc.has_multiindex(axis):
+                # `Index.get_locs` raises an IndexError by itself if missing labels were provided,
+                # we don't have to do missing-check for the received `axis_lookup`.
+                if isinstance(axis_loc, pandas.MultiIndex):
+                    axis_lookup = self.qc.get_axis(axis).get_indexer_for(axis_loc)
+                else:
+                    axis_lookup = self.qc.get_axis(axis).get_locs(axis_loc)
+            elif is_boolean_array(axis_loc):
+                axis_lookup = boolean_mask_to_numeric(axis_loc)
             else:
-                col_lookup = self.qc.columns.get_locs(col_loc)
-        elif is_boolean_array(col_loc):
-            # If passed in a list of booleans, we return the index of the true values
-            col_lookup = [i for i, col_val in enumerate(col_loc) if col_val]
-        else:
-            col_lookup = self.qc.columns.get_indexer_for(col_loc)
-        return row_lookup, col_lookup
+                axis_labels = self.qc.get_axis(axis)
+                if is_list_like(axis_loc) and not isinstance(
+                    axis_loc, (np.ndarray, pandas.Index)
+                ):
+                    # `Index.get_indexer_for` works much faster with numpy arrays than with python lists,
+                    # so although we lose some time here on converting to numpy, `Index.get_indexer_for`
+                    # speedup covers the loss that we gain here.
+                    axis_loc = np.array(axis_loc, dtype=axis_labels.dtype)
+                axis_lookup = axis_labels.get_indexer_for(axis_loc)
+                # `Index.get_indexer_for` sets -1 value for missing labels, we have to verify whether
+                # there are any -1 in the received indexer to raise a KeyError here.
+                missing_mask = axis_lookup < 0
+                if missing_mask.any():
+                    missing_labels = (
+                        # Converting `axis_loc` to maskable `np.array` to not fail
+                        # on masking non-maskable list-like
+                        np.array(axis_loc)[missing_mask]
+                        if is_list_like(axis_loc)
+                        else axis_loc
path: modin/pandas/indexing.py
text: @user1:
why we return unchanged `axis_loc` if it isn't list-like, but there are missing labels?

@author:
added an in-code comment about this:
```python
                        # If `axis_loc` is not a list-like then we can't select certain
                        # labels that are missing and so printing the whole indexer
```

diff_hunk: @@ -175,6 +201,32 @@ def is_range_like(obj):
     )
 
 
+def boolean_mask_to_numeric(indexer):
+    """
+    Convert boolean mask to numeric indices.
+
+    Parameters
+    ----------
+    indexer : list-like of booleans
+
+    Returns
+    -------
+    np.ndarray of ints
+        Numerical positions of ``True`` elements in the passed `indexer`.
+    """
+    if isinstance(indexer, (np.ndarray, Series, pandas.Series)):
+        return np.where(indexer)[0]
+    else:
+        # It's faster to build the resulting numpy array from the reduced amount of data via
+        # `compress` iterator than convert non-numpy-like `indexer` to numpy and apply `np.where`.
+        return np.fromiter(
+            # `itertools.compress` masks `data` with the `selectors` mask,
+            # works about ~10% faster than a pure list comprehension
+            compress(data=range(len(indexer)), selectors=indexer),
path: modin/pandas/indexing.py
text: @user1:
please use full name (and change `import` accordingly) to `itertools.compress()` for better reader's understanding

@author:
done