?

Log in

No account? Create an account

"Как много нам открытий чудных...." - Узором созвездий по мантии ночи

29.11.2013, Пятница

17:00:00 - "Как много нам открытий чудных...."

Previous Entry Поделиться Next Entry

Видим в чужом коде такую функцию и ужасаемся:

def reprList( _list, divider=',' ):
    param_string = ""
    if _list:
        for val in _list:
            param_string += repr(val) + divider
        return param_string[:-(len(divider))]
    return param_string

Тут же делаем свой вариант, который и короче, и понятнее:
def reprL( list_, divider=',' ):
    return divider.join(repr(val) for val in list_)


Затем берём модуль timeit, пробуем на нескольких примерах, и радость наша сменяется озадаченностью:


[]
reprList: 0.285995960236
reprL: 2.35047221184

(123, {'q': 'wleiweiruw', 12: 'asaqw'}, 'DWERwerwerwsdgerte', set([1, 2, 3, 4, 5]), 923842982)
reprList: 6.62340903282
reprL: 8.48656201363

[123, {'q': 'wleiweiruw', 12: 'asaqw'}, 'DWERwerwerwsdgerte', set([1, 2, 3, 4, 5]), 923842982]
reprList: 6.54594492912
reprL: 8.45274496078

['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'reprL', 'reprList', 'timeit']
reprList: 3.84632706642
reprL: 4.83487200737

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127]
reprList: 44.1687018871
reprL: 34.8512501717

['Timer', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '_template_func', 'default_number', 'default_repeat', 'default_timer', 'dummy_src_name', 'gc', 'itertools', 'main', 'reindent', 'repeat', 'sys', 'template', 'time', 'timeit']
reprList: 9.64470481873
reprL: 9.28048801422


Н-да... создание итератора и передача его в join() отжрает времени больше, чем лишние локальные переменные и отрезание хвостового пробела в первом варианте функции, и выигрыш от join по сравненю с += перевешивает его только где-то с 20 элементов списка.

Несколько печалит, что компактная конструкция, сделанная как раз в стиле этого языка и на библиотечных методах тащит столько накладных расходов, что порой самостоятельно написанный "общеязыковой" вариант у неё выигрывает.

(Ещё несколько удивляет, что цикл и итератор по списку работают чуть быстрее, чем по кортежу, ну да ладно.)


This entry was originally posted at http://arilou.dreamwidth.org/931852.html. Please comment there using OpenID.

Comments:

[User Picture]
From:qkowlew
Date:29.11.2013 13:49:31
(Link)
А когда-то ещё дозволялось компилить с разными ключиками оптимизации...

Разница быстродействия в 80 раз в работе с библиотечной (!) функцией внутри цикла в пользу НЕоптимизированного кода меня когда-то озадачила намного круче. :) И это был всего лишь Си.
(Ответить) (Thread)
[User Picture]
From:arilou
Date:29.11.2013 14:08:40
(Link)
А причина?
(Ответить) (Parent) (Thread)
[User Picture]
From:qkowlew
Date:29.11.2013 22:40:09
(Link)
Оптимизирующий код, создававшийся на заре Vax/PDP эпохи, содержал стремление засунуть часто используемые, а особенно локальные переменные в регистры.

Другой кусок кода оптимизатора изначально содержал табличку "СКОЛЬКО регистров ест какая оптимизированная библиотечная функция".

Однако речь о i8086 компиляторе. И это "сколько" оказывается очень странным, имеющим малое отношение к реальности числом.

При выключенной оптимизации код внутри цикла не порождал локальных регистровых переменных, даже если его об этом просили.

Результатом оптимизации становилось
- используем регистровую переменную в аккумуляторе
- используем вторую регистровую переменную в каком-нибудь счётчике
- натыкаемся на функцию, про которую сказано, что у ей внутре больше регистровых переменных, чем "свободных" регистров
- пихаем регистр в стек
- пихаем регистр в стек
- вызываем функцию
- используем её значение... И если оно взаимодействует с ранее запихнутыми в стек переменными, то нимножка шевелим этот стек туда-сюда.

Вроде бы ничего криминального. Но в результате число бессмысленных перепихиваний стека оказывается каким-то фантастическим.

Ессно, это уже не дословно - как помню.


Edited at 2013-11-29 22:42 (UTC)
(Ответить) (Parent) (Thread)
[User Picture]
From:mcjabberwock
Date:29.11.2013 17:48:34
(Link)
Забавно. А какая функция - сейчас уже не вспомнить?
(Ответить) (Parent) (Thread)
[User Picture]
From:qkowlew
Date:29.11.2013 22:41:50
(Link)
честно не помню, но реально это не очень критично.
Виноват не код функции, а код, генеримый "вокруг вызова функции" оптимизатором.
(Ответить) (Parent) (Thread)
[User Picture]
From:arilou
Date:29.11.2013 15:01:02
(Link)
Забавно в добавок, что
def reprLs( _list, divider=',' ):
    if _list:
        param_string = repr(_list[0])
        for val in _list[1:]:
            param_string += divider + repr(val)
        return param_string
    return ''

оказывается лучше первого лишь на очень коротких (до 10 элементов) списках, немного отстаёт от первого по скорости на средних списках (когда вариант с join() ещё остаётся худшим) и хуже всех на "длинных" (т.е. когда вариант с join() выходит вперёд, а это около 20 элементов и более) списках.
Такое ощущение, что for val in _list[1:] делает копию списка с 1-го элемента.
А как указать, чтобы for пошёл по списку не с начала, но именно по самому списку, не делая копию?
(Ответить) (Thread)
[User Picture]
From:phd
Date:29.11.2013 15:37:42
(Link)
> Такое ощущение, что for val in _list[1:] делает копию списка с 1-го элемента.

Совершенно верно, делает копию.

> А как указать, чтобы for пошёл по списку не с начала, но именно по самому списку, не делая копию?

Никак. Меняй алгоритм. Например, делай цикл for val in _list, но первый элемент пропусти.
(Ответить) (Parent) (Thread)
[User Picture]
From:arilou
Date:09.12.2013 10:11:03
(Link)
Это как это "первый элемент пропусти"? Цикл по enumerate(_list) и 'if i > 0:' внутри цикла? Или создать итератор и сделать в начале один холостой .next()? Второе пробовал, тоже не очень впечатлило.
(Ответить) (Parent) (Thread)
[User Picture]
From:phd
Date:09.12.2013 11:12:24
(Link)
Ну или вот, например:
first = True
for elem in list:
    if first:
        first = False
    else:
        process(elem)
(Ответить) (Parent) (Thread)
[User Picture]
From:arilou
Date:10.12.2013 14:57:07
(Link)
Лишняя проверка в цикле.
Зашибись оптимизация.
Тогда уж лучше
li = iter(list)
li.next()
for elem in li:
    process(elem)
(Ответить) (Parent) (Thread)
[User Picture]
From:phd
Date:10.12.2013 15:08:08
(Link)
Померяй и напиши, какой вариант лучше. На коротких списках и на длинных.
(Ответить) (Parent) (Thread)
[User Picture]
From:iliaworld
Date:29.11.2013 18:14:02
(Link)
А если использовать map?

divider.join(map(repr, _list))
(Ответить) (Thread)
[User Picture]
From:arilou
Date:09.12.2013 10:27:27
(Link)
"Слона-то я и не приметил..."

Да, вариант с map() проигрывает первому ~5% на списке из одного элемента, там же и ещё на списке из двух проигрывает не приведённому тут варианту, приведёному ниже, а дальше везде бесспорный лидер.
def reprLs(_list, divider=','):
    if _list:
        param_string = repr(_list[0])
        for val in _list[1:]:
            param_string += divider + repr(val)
        return param_string
    return ''



Edited at 2013-12-09 10:44 (UTC)
(Ответить) (Parent) (Thread)