Parallel Deposit / Bit Scatter в Python. Есть ли оно вообще?
На этой неделе Литкод кошмарит индейцев битовыми операциями, в которые они не умеют.
https://leetcode.com/problems/minimum-array-end/description/
Нужно найти минимально возможное n-1-ое число строго возрастающей последовательности положительных чисел, AND всех элементов которой даёт заданное число x. Понятно, что нужно просто биты раскидать в свободные слоты этого самого икса.
Собственно, задача не особо сложная, минут за 15 решается при помощи интринсиков (вообще, за минуту, но не в моём возрасте и не в 4 часа ночи). Пока индийские друзья писали свои любимые циклы, слепил я на эту тему однострочник примерно такого вида:
#include <immintrin.h>
#define BMI2 __attribute__((__target__("bmi2")))
class Solution {
public:
static long long minEnd(const uint n, const uint x) BMI2 {
return x | _pdep_u64(n - 1u, ~uint64_t(x));
}
};
(для C и Rust - аналогично)
Java тоже приятно удивила. Мало того, что нашёлся метод, так он ещё и интринсифицирован в OpenJDK (и их JDK как раз LC и использует):
class Solution {
public static long minEnd(final int n, final int x) {
return x | Long.expand(n - 1, ~(long)x);
}
}
А вот в Питоне так не получается. Цикл, конечно, работает (медленная ручная реализация PDEP):
class Solution:
@staticmethod
def minEnd(n: int, x: int) -> int:
n -= 1
c, m, mask = 0, 1, x ^ ((1 << (n.bit_length() + x.bit_length())) - 1)
while mask:
b = mask & -mask
if n & m:
c |= b
mask -= b
m <<= 1
return x | c
и даже светит 0 ms / 100% в итогах выполнения.
Но есть ли способ решить это однострочником, без циклов, как в нормальных языках?
А тут - решение со ссылками на код. Всё, кроме Java, исполняется достаточно шустренько:
https://leetcode.com/problems/minimum-array-end/solutions/6024965/bit-deposit-one-liner-beating-100-in-c-c-rust-and-python/
Нет, в стандартном Python не существует встроенного аналога PDEP (Parallel Deposit / Bit Scatter), и достичь однострочного решения без каких-либо циклов или обходных манёвров не получится; никакой готовой интринсики, аналогичной C++/Rust или Java, нет.
Здравствуйте, Папа! Сегодня Вы ответили на мой последний вопрос про Python, где оставили интересный ресурс, но сделали это под ответом очень странного чувака, который сначала до бесконечности изменял свой ответ, а потом и вовсе удалил. Не могу снова найти, не могли бы продублировать ссылку здесь? Буду очень благодарен!
Единственный вариант — замаскировать рекурсию (или скрытый цикл в виде генератора/редукции) в однострочник. Например, можно написать так:
min_end = lambda n, x: x | (lambda f, v, m: 0 if m == 0 else ((v & 1) * (m & -m)) | f(f, v >> 1, m & (m - 1)))(lambda f, v, m: 0 if m == 0 else ((v & 1) * (m & -m)) | f(f, v >> 1, m & (m - 1)), n - 1, ~x)
В ванильном петухоне “готового” аналога инструкций типа _pdep_u64 нет, и никаким встроенным однострочником (по аналогии с C++ Intrinsic или Java `Long.expand`) это не решить. При желании можно либо:
1. Сделать ручную реализацию (циклом, через reduce, через побитовые трюки со строками и т.д.).
2. Использовать внешнюю библиотеку (C-расширение, ctypes/cffi, numba и т.п.), которая внутри вызывает машинную инструкцию BMI2, если ваша платформа её поддерживает.
Почему так?
В языках типа C/C++ или Rust под x86_64 это решается вызовом _intrinsic-функций_ (_pdep_u64, _pext_u64 и т.д.), в Java – методом Long.expand(...), а в ассемблерном коде – прямой инструкцией `PDEP`. Всё это завязано на конкретные аппаратные возможности процессора (BMI2).
CPython же не предоставляет таких “сырых” CPU-инструкций напрямую. Основная причина – дизайн языка:
- Python стремится дать высокоуровневый API, а низкоуровневые аппаратно-зависимые вещи переносит либо в C-уровень рантайма, либо отдаёт на откуп сторонним модулям.
- Далеко не все платформы (ARM, PowerPC и т.д.) имеют аналог PDEP/PEXT в той же форме, да и на x86 не у всех процессоров есть BMI2.
Поэтому в “ванильном” Python вы обычно либо крутите побитовые операции в цикле, либо (для скорости) переходите на C/сffi/numba и т. п.
---
Ваш пример с циклом – классическая побитовая раскладка (часто её и называют «ручным PDEP»):
def pdep_python(n: int, mask: int) -> int:
out = 0
bitpos = 1
while mask:
b = mask & -mask
mask ^= b # Убираем его из mask
if n & bitpos:
out |= b
bitpos <<= 1
return out
Применение для задачи «разложить (n - 1) по свободным битам `~x` и “слить” с самим x»:
def minEnd(n: int, x: int) -> int:
return x | pdep_python(n - 1, ~x)
Можно завернуть это в однострочник, но по сути это всё равно будет функционально тот же цикл, просто записанный “в одну строку” через functools.reduce или неявную рекурсию. Например (не рекомендую в продакшене), что-нибудь в стиле:
from functools import reduce
def pdep_oneliner(n, mask):
return reduce(
lambda acc_bpos, _: (
(acc_bpos[0] | (acc_bpos[1] & -(acc_bpos[1])) if (n & acc_bpos[2]) else acc_bpos[0]),
acc_bpos[1] & (acc_bpos[1] - 1),
acc_bpos[2] << 1
),
range(mask.bit_count()),
(0, mask, 1)
)[0]
Но выглядит это, мягко говоря, хуже, чем обычный цикл.
---
Можно ли ускорить без ручного цикла?
1. Numba. Если вы используете Numba (JIT-компилятор для Python), то вручную написанный цикл может JIT’иться в машинный код, и там может быть распознана конструкция, которую оптимизатор превратит в “аппаратный” PDEP. Однако гарантировать, что именно так произойдёт, нельзя – зависит от версии LLVM и множества внутренних деталей.
2. Стороннее C-расширение (через ctypes или cffi), в котором вызвать _pdep_u64 напрямую (на x86_64 с BMI2).
3. Попробовать “строковые” хаки. Теоретически, можно пытаться раскладывать биты через преобразование в бинарную строку, индексирование и т. д. Но это, как правило, ещё медленнее, чем цикл, да и выглядит громоздко.
---
Так что если хочется короткий и быстрый PDEP – его придётся или имитировать ручной побитовой раскладкой, или вызывать через библиотеку, которая внутри делает аппаратный PDEP (на платформах, где это есть). В стандартной библиотеке ничего подобного пока нет.
class Solution:
@staticmethod
def minEnd(n: int, x: int) -> int:
return x | sum(((n - 1) >> i & 1) << p for p, i in enumerate(p for p in range(x.bit_length() + (n - 1).bit_length()) if not x & (1 << p)))