11# Authors: The scikit-learn developers
22# SPDX-License-Identifier: BSD-3-Clause
33
4+ from math import log2, ceil
5+
46from cython.parallel import prange
57from libc.math cimport isnan
68
@@ -49,18 +51,23 @@ def _map_to_bins(const X_DTYPE_C [:, :] data,
4951
5052cdef void _map_col_to_bins(
5153 const X_DTYPE_C [:] data,
52- const X_DTYPE_C [:] binning_thresholds,
54+ const X_DTYPE_C [:: 1 ] binning_thresholds,
5355 const uint8_t is_categorical,
5456 const uint8_t missing_values_bin_idx,
5557 int n_threads,
56- X_BINNED_DTYPE_C [:] binned
58+ X_BINNED_DTYPE_C [:: 1 ] binned
5759):
5860 """ Binary search to find the bin index for each value in the data."""
5961 cdef:
6062 int i
61- int left
62- int right
63- int middle
63+ uint8_t n_iter_bin_search
64+ size_t n_bin_thresholds
65+
66+ n_bin_thresholds = len (binning_thresholds)
67+ if n_bin_thresholds == 0 :
68+ n_iter_bin_search = 0
69+ else :
70+ n_iter_bin_search = int (ceil(log2(n_bin_thresholds)))
6471
6572 for i in prange(data.shape[0 ], schedule = ' static' , nogil = True ,
6673 num_threads = n_threads):
@@ -73,13 +80,43 @@ cdef void _map_col_to_bins(
7380 binned[i] = missing_values_bin_idx
7481 else :
7582 # for known values, use binary search
76- left, right = 0 , binning_thresholds.shape[0 ]
77- while left < right:
78- # equal to (right + left - 1) // 2 but avoids overflow
79- middle = left + (right - left - 1 ) // 2
80- if data[i] <= binning_thresholds[middle]:
81- right = middle
82- else :
83- left = middle + 1
84-
85- binned[i] = left
83+ binned[i] = _binary_search(
84+ data[i],
85+ binning_thresholds,
86+ n_bin_thresholds,
87+ n_iter_bin_search
88+ )
89+
90+
91+ cdef inline size_t _binary_search(
92+ X_DTYPE_C value,
93+ const X_DTYPE_C [::1 ] binning_thresholds,
94+ size_t size,
95+ uint8_t n_iterations
96+ ) noexcept nogil:
97+ cdef:
98+ size_t left
99+ size_t half
100+ size_t middle
101+ size_t remaining_size
102+
103+ # This implementation is designed to minimize branch mispredictions. See:
104+ # https://pvk.ca/Blog/2012/07/03/binary-search-star-eliminates-star-branch-mispredictions/
105+ # https://pvk.ca/Blog/2015/11/29/retrospective-on-binary-search-and-on-compression-slash-compilation/
106+ left = 0
107+ remaining_size = size
108+
109+ # Fixed number of loops, instead of less-predictable while loop:
110+ for _ in range (n_iterations):
111+ half = remaining_size / 2
112+ middle = left + half
113+ # Try for cmov instead of branch; see
114+ # https://en.algorithmica.org/hpc/pipelining/branchless/ for details:
115+ left = middle if (binning_thresholds[middle] < value) else left
116+ remaining_size -= half
117+
118+ # Try for cmov instead of branch:
119+ left = left + 1 if (
120+ (left < size) and (value > binning_thresholds[left])
121+ ) else left
122+ return left
0 commit comments