`if key в dict` против `pop` плюс `try/except KeyError` в Cython – быстрое (лучшее) решение?

Очень похоже на этот вопрос. Мне было интересно, что более идиоматично и быстрее при использовании Cython: блок try/except KeyError или альтернативный решения на основе pop и/или in.


Данные испытаний:

from random import shuffle, randint

datal = list(map(float, range(1_000)))
shuffle(datal)
data = {
    a * 10 ** (randint(0, 2) * 3): b
    for a, b in zip(datal[:-1], datal[1:])
}
data[datal[-1] * 10 ** (randint(0, 2) * 3)] = datal[0]

Три варианта на Cython (уровень языка 3):

  1. В основном динамическая типизация, pop и None в качестве возвращаемого значения по умолчанию:
def test_none(dict raw):
    _, last_value = raw.popitem()
    cdef list out = [last_value]
    while len(raw) > 0:
        next_value = raw.pop(last_value, None)
        if next_value is not None:
            out.append(next_value)
            last_value = next_value
            continue
        next_value = raw.pop(last_value * 1_000, None)
        if next_value is not None:
            last_value = next_value
            out.append(next_value)
            continue
        next_value = raw.pop(last_value * 1_000_000)
        out.append(next_value)
        last_value = next_value
    return out
  1. С in и pop:
def test_in(dict raw):
    cdef double last_value, next_value, _
    _, last_value = raw.popitem()
    cdef list out = [last_value]
    while len(raw) > 0:
        if last_value in raw:
            next_value = raw.pop(last_value)
        elif last_value * 1_000 in raw:
            next_value = raw.pop(last_value * 1_000)
        else:
            next_value = raw.pop(last_value * 1_000_000)
        out.append(next_value)
        last_value = next_value
    return out
  1. С pop и try/except KeyError:
def test_te(dict raw):
    cdef double last_value, next_value, _
    _, last_value = raw.popitem()
    cdef list out = [last_value]
    while len(raw) > 0:
        try:
            next_value = raw.pop(last_value)
        except KeyError:
            try:
                next_value = raw.pop(last_value * 1_000)
            except KeyError:
                next_value = raw.pop(last_value * 1_000_000)
        out.append(next_value)
        last_value = next_value
    return out

С timeit я получаю следующие результаты:

%timeit test_te(data.copy())
471 µs ± 1.57 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit test_in(data.copy())
219 µs ± 9.04 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit test_none(data.copy())
175 µs ± 401 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

person s-m-e    schedule 13.06.2021    source источник
comment
Я не очень понимаю вопрос. Вы ответили на свой вопрос о производительности другого подхода. При этом test_none и test_in выглядят нормально. Второй, вероятно, более идиоматичен, но люди могут не согласиться. В любом случае, использование исключения на критическом пути горячего цикла никогда не будет хорошей идеей. Обычно это медленно и не идиоматично, поскольку исключения должны быть редкими, а не обычными (отсюда и название).   -  person Jérôme Richard    schedule 13.06.2021
comment
@JérômeRichard Спасибо. В каком-то смысле я сам на него ответил. И затем я вижу совершенно другое поведение в большей кодовой базе. Я выделил соответствующий путь кода и реализовал все три решения, как указано выше. Все они почти одинаково быстры, причем try/except на самом деле самые быстрые (!). Я использую кортежи целых чисел в качестве ключа и объекты, поступающие из класса расширения, в качестве значений - вот в чем различия. Поэтому я не уверен, что на самом деле лучше - конкретно, почему и при каких обстоятельствах.   -  person s-m-e    schedule 13.06.2021
comment
Не будет ли это зависеть от частоты совпадений? Например, я предполагаю (я определенно ничего здесь не измерял), что версия try/except, вероятно, будет хороша, когда неудачные поиски очень редки, и плоха, когда они распространены.   -  person DavidW    schedule 13.06.2021
comment
@DavidW Хороший вопрос. В моем примере выше все три ветви распределены поровну. В моей большой кодовой базе я просматриваю их от очень вероятного до очень маловероятного. Так что я не натыкаюсь на столько блоков except, как в моем маленьком тесте (относительно). Имеет смысл.   -  person s-m-e    schedule 13.06.2021
comment
@DavidW действительно, кто-то задокументировал это: wiki.python.org/moin/PythonSpeed/   -  person Oppen    schedule 30.06.2021