Почему сортировка списка кортежей Python выполняется быстрее, если я явно указываю ключ в качестве первого элемента?

Сортировка списка кортежей (ключи словаря, пары значений, где ключ является случайной строкой) выполняется быстрее, если я явно не указываю, что следует использовать ключ (изменить: добавлен оператор.itemgetter (0) из комментарий @Chepner, и ключевая версия теперь работает быстрее!):

import timeit

setup ="""
import random
import string

random.seed('slartibartfast')
d={}
for i in range(1000):
    d[''.join(random.choice(string.ascii_uppercase) for _ in range(16))] = 0
"""
print min(timeit.Timer('for k,v in sorted(d.iteritems()): pass',
        setup=setup).repeat(7, 1000))
print min(timeit.Timer('for k,v in sorted(d.iteritems(),key=lambda x: x[0]): pass',
        setup=setup).repeat(7, 1000))
print min(timeit.Timer('for k,v in sorted(d.iteritems(),key=operator.itemgetter(0)): pass',
        setup=setup).repeat(7, 1000))

Дает:

0.575334150664
0.579534521128
0.523808984422 (the itemgetter version!)

Однако если я создам настраиваемый объект, явно передавая key=lambda x: x[0] sorted, это ускорит:

setup ="""
import random
import string

random.seed('slartibartfast')
d={}

class A(object):
    def __init__(self):
        self.s = ''.join(random.choice(string.ascii_uppercase) for _ in
              range(16))
    def __hash__(self): return hash(self.s)
    def __eq__(self, other):
        return self.s == other.s
    def __ne__(self, other): return self.s != other.s
    # def __cmp__(self, other): return cmp(self.s ,other.s)

for i in range(1000):
    d[A()] = 0
"""
print min(timeit.Timer('for k,v in sorted(d.iteritems()): pass',
        setup=setup).repeat(3, 1000))
print min(timeit.Timer('for k,v in sorted(d.iteritems(),key=lambda x: x[0]): pass',
        setup=setup).repeat(3, 1000))
print min(timeit.Timer('for k,v in sorted(d.iteritems(),key=operator.itemgetter(0)): pass',
        setup=setup).repeat(3, 1000))

Дает:

4.65625458083
1.87191002252
1.78853626684

Ожидается ли это? Похоже, что второй элемент кортежа используется во втором случае, но разве ключи не должны сравниваться неравно?

Примечание: раскомментирование метода сравнения дает худшие результаты, но все же время составляет половину:

8.11941771831
5.29207000173
5.25420037046

Как и ожидалось, встроенный (сравнение адресов) работает быстрее.

ИЗМЕНИТЬ: вот результаты профилирования моего исходного кода, который вызвал вопрос - без ключевого метода:

         12739 function calls in 0.007 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.007    0.007 <string>:1(<module>)
        1    0.000    0.000    0.007    0.007 __init__.py:6527(_refreshOrder)
        1    0.002    0.002    0.006    0.006 {sorted}
     4050    0.003    0.000    0.004    0.000 bolt.py:1040(__cmp__) # here is the custom object
     4050    0.001    0.000    0.001    0.000 {cmp}
     4050    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 {method 'sort' of 'list' objects}
      291    0.000    0.000    0.000    0.000 __init__.py:6537(<lambda>)
      291    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 bolt.py:1240(iteritems)
        1    0.000    0.000    0.000    0.000 {method 'iteritems' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

и вот результаты, когда я указываю ключ:

         7027 function calls in 0.004 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.004    0.004 <string>:1(<module>)
        1    0.000    0.000    0.004    0.004 __init__.py:6527(_refreshOrder)
        1    0.001    0.001    0.003    0.003 {sorted}
     2049    0.001    0.000    0.002    0.000 bolt.py:1040(__cmp__)
     2049    0.000    0.000    0.000    0.000 {cmp}
     2049    0.000    0.000    0.000    0.000 {isinstance}
        1    0.000    0.000    0.000    0.000 {method 'sort' of 'list' objects}
      291    0.000    0.000    0.000    0.000 __init__.py:6538(<lambda>)
      291    0.000    0.000    0.000    0.000 __init__.py:6533(<lambda>)
      291    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 bolt.py:1240(iteritems)
        1    0.000    0.000    0.000    0.000 {method 'iteritems' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Очевидно, вызывается метод __cmp__, а не __eq__ (изменить, потому что этот класс определяет __cmp__, но не __eq__, см. здесь для порядка разрешения равных и сравните).

В приведенном здесь коде метод __eq__ действительно вызывается (8605 раз), что видно по добавлению отладочных отпечатков ( см. комментарии).

Итак, разница указана в ответе @chepner. Последнее, что мне не совсем понятно, - это зачем эти вызовы равенства кортежей (IOW, почему нам нужно вызывать eq, а мы не вызываем cmp напрямую).

ОКОНЧАТЕЛЬНОЕ РЕДАКТИРОВАНИЕ: Я задал этот последний вопрос здесь: Почему при сравнении кортежей объектов python вызывается __eq__, а затем __cmp__ ? - оказывается, это оптимизация, сравнение кортежей вызывает __eq__ в элементах кортежа и вызывает cmp только для элементов кортежа, а не eq. Итак, теперь это совершенно ясно. Я думал, что он вызывает напрямую __cmp__, поэтому сначала мне показалось, что указывать ключ просто не нужно, и после ответа Чепнера я все еще не понимал, откуда приходят равные вызовы.

Смысл: https://gist.github.com/Utumno/f3d25e0fe4bd0f43ceb9178a60181a53


person Mr_and_Mrs_D    schedule 24.12.2015    source источник
comment
Использование lambda x: x[0] учитывает только первый элемент   -  person Padraic Cunningham    schedule 24.12.2015
comment
@ That1Guy, извините, только что удалил комментарий по ошибке, вы говорите о скорости c vs python, поэтому вы получите забитые выше методы на чистом python   -  person Padraic Cunningham    schedule 24.12.2015
comment
@ That1Guy, основная разница здесь в том, что если вы добавите print(self, other ) в eq, вы не увидите, что он вообще вызывается для лямбда-решения, для не лямбда-решения он вызывается 88 раз, поэтому у вас есть 88 медленных вызовов методов Python   -  person Padraic Cunningham    schedule 24.12.2015
comment
Сравнение случайных объектов одного и того же типа должно привести к сравнению их адресов (в CPython), поэтому первые элементы должны сравниваться неравными в обеих настройках - отредактируем, сделав ключи 16-символьными, чтобы они всегда сравнивали неравные в обеих настройках - одинаковые результаты. Таким образом, кажется, что второй элемент не понадобится при сравнении - так почему же он ускоряется, явно опуская его во втором случае? @PadraicCunningham: __eq__ Вы верите? Почему он вызывается в случае, отличном от лямбда?   -  person Mr_and_Mrs_D    schedule 24.12.2015
comment
@Mr_and_Mrs_D, в случае, отличном от лямбда, вызывается eq, это то, что вас убивает.   -  person Padraic Cunningham    schedule 24.12.2015
comment
@Padraic Cunningham: но почему это называется? Я ожидал бы сравнить первые элементы кортежа в обоих случаях (лямбда и не лямбда), и эти первые элементы сравниваются неравно в обоих случаях, поэтому я ожидал бы одинаковой производительности. Есть ли разница в сравнении кортежей, и если да, то почему? Итак, я могу представить, что не лямбда примерно эквивалентна cmp= lambda x, y: x[0] < y[0] or x[1] < y[1] - где eq здесь?   -  person Mr_and_Mrs_D    schedule 24.12.2015
comment
Ваш первый пример, кажется, показывает противоположное тому, что вы утверждаете.   -  person Adam Smith    schedule 24.12.2015
comment
@AdamSmith: на самом деле да - изменилось, когда я переключился со случайных строк на случайные целые числа. Возврат к версии со случайной строкой. Случайная версия ints: stackoverflow.com/revisions/   -  person Mr_and_Mrs_D    schedule 24.12.2015


Ответы (1)


Здесь есть две проблемы.

  1. Сравнение двух значений встроенных типов (таких как int) происходит в C. Сравнение двух значений класса с методом __eq__ происходит в Python; многократный вызов __eq__ приводит к значительному снижению производительности.

  2. Функция, переданная с key, вызывается один раз для каждого элемента, а не один раз для сравнения. Это означает, что lambda x: x[0] вызывается один раз для построения списка A экземпляров для сравнения. Без key вам нужно провести O (n lg n) сравнений кортежей, каждое из которых требует вызова A.__eq__ для сравнения первого элемента каждого кортежа.

Первый объясняет, почему ваша первая пара результатов меньше второй, а вторая занимает несколько секунд. Второй объясняет, почему использование key выполняется быстрее независимо от сравниваемых значений.

person chepner    schedule 24.12.2015
comment
Спасибо - почему в моей отредактированной версии int версия ключа работает быстрее: stackoverflow.com/revisions/ а здесь на самом деле медленнее? Также x [0] являются экземплярами A, и мне все еще нужны сравнения O (nlogn), чтобы отсортировать список, возвращаемый с использованием лямбда-ключа - разве в тех сравнениях __eq__ не вызывается? - person Mr_and_Mrs_D; 24.12.2015
comment
Это все еще O (n lg n), но с меньшей константой. (Вместо того, чтобы делать O (n lg n) сравнений кортежей и O (n lg n) A сравнений, вы выполняете O (n) ключевых поисков и O (n lg n) A сравнений). - person chepner; 24.12.2015
comment
Ага - спасибо - значит eq вызывается при сравнении кортежей? Почему тогда ключевой пример в вопросе медленнее в первом случае? - person Mr_and_Mrs_D; 24.12.2015
comment
Кортежи и целые числа сравниваются в C; никаких дополнительных функций Python не вызывается. Вызов лямбда-выражения для извлечения первого целого числа из кортежа происходит медленнее, чем прямое сравнение кортежей. Попробуйте вместо этого использовать key=operator.itemgetter(0) (конечно, после импорта operator). - person chepner; 24.12.2015
comment
Это быстрее в случае целых чисел: stackoverflow.com/revisions/, но я угадайте, потому что там кортеж eq завершен в обход (некоторая оптимизация для целочисленных кортежей?) Последний бит, который мне все еще не ясен, зачем нам нужен дополнительный O (n lg n) __eq__ в неключевом случае? - person Mr_and_Mrs_D; 25.12.2015
comment
Хорошо, наконец, я полностью понял это - ингредиент, которого мне не хватало, заключается в том, что сравнение кортежей не вызывает __cmp__ непосредственно в элементах кортежа - оно сначала вызывает __eq__: stackoverflow.com/q/37513671/281545 - теперь комментарии выше имеют смысл - person Mr_and_Mrs_D; 30.05.2016