"Как много нам открытий чудных...." - Узором созвездий по мантии ночи
29.11.2013, Пятница
17:00:00 - "Как много нам открытий чудных...."
Видим в чужом коде такую функцию и ужасаемся:
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.
Разница быстродействия в 80 раз в работе с библиотечной (!) функцией внутри цикла в пользу НЕоптимизированного кода меня когда-то озадачила намного круче. :) И это был всего лишь Си.
Другой кусок кода оптимизатора изначально содержал табличку "СКОЛЬКО регистров ест какая оптимизированная библиотечная функция".
Однако речь о i8086 компиляторе. И это "сколько" оказывается очень странным, имеющим малое отношение к реальности числом.
При выключенной оптимизации код внутри цикла не порождал локальных регистровых переменных, даже если его об этом просили.
Результатом оптимизации становилось
- используем регистровую переменную в аккумуляторе
- используем вторую регистровую переменную в каком-нибудь счётчике
- натыкаемся на функцию, про которую сказано, что у ей внутре больше регистровых переменных, чем "свободных" регистров
- пихаем регистр в стек
- пихаем регистр в стек
- вызываем функцию
- используем её значение... И если оно взаимодействует с ранее запихнутыми в стек переменными, то нимножка шевелим этот стек туда-сюда.
Вроде бы ничего криминального. Но в результате число бессмысленных перепихиваний стека оказывается каким-то фантастическим.
Ессно, это уже не дословно - как помню.
Edited at 2013-11-29 22:42 (UTC)
Виноват не код функции, а код, генеримый "вокруг вызова функции" оптимизатором.
оказывается лучше первого лишь на очень коротких (до 10 элементов) списках, немного отстаёт от первого по скорости на средних списках (когда вариант с join() ещё остаётся худшим) и хуже всех на "длинных" (т.е. когда вариант с join() выходит вперёд, а это около 20 элементов и более) списках.
Такое ощущение, что
for val in _list[1:]
делает копию списка с 1-го элемента.А как указать, чтобы for пошёл по списку не с начала, но именно по самому списку, не делая копию?
Совершенно верно, делает копию.
>
Никак. Меняй алгоритм. Например, делай цикл for val in _list, но первый элемент пропусти.
Зашибись оптимизация.
Тогда уж лучше
divider.join(map(repr, _list))
Да, вариант с map() проигрывает первому ~5% на списке из одного элемента, там же и ещё на списке из двух проигрывает не приведённому тут варианту, приведёному ниже, а дальше везде бесспорный лидер.
Edited at 2013-12-09 10:44 (UTC)