Подсчет длины последовательности Коллатца - пользовательский итератор замедляет работу?

Я решал задачу UVA №100 — "The 3n + 1 проблема". Это их «примерная» задача с очень щадящим ограничением по времени (ограничение в 3 секунды, их пример решения вообще без кэширования выполняется за 0,738 с, мое лучшее решение на данный момент работает за 0,016 с), поэтому я подумал, что, как бы я ни экспериментировал с кодом, я всегда должен укладываться в лимит. Что ж, я ошибся.

Спецификация задачи проста: каждая строка ввода содержит два числа i и j, а вывод должен вывести эти числа, за которыми следует максимальная длина последовательности Коллатца, начало которой находится между i и j включительно.

Я подготовил четыре решения. Они очень похожи, и все дают хорошие ответы.

Первое решение кэширует последовательности до 0x100000 длин в vector. Длина 0 означает, что длина последовательности, начинающейся с этого конкретного числа, еще не рассчитана. Работает достаточно быстро - 0,03 сек.

Второе решение очень похоже, только оно кэширует каждую отдельную длину в разреженном массиве, реализованном с помощью unordered_map. Оно работает довольно медленнее, чем предыдущее решение, но все же хорошо укладывается в ограничение: 0,28 с.

В качестве упражнения я также написал третье решение, основанное на втором. Цель состояла в том, чтобы использовать функцию max_element, которая принимает только итераторы. Я не мог использовать unordered_map::iterator, для увеличения такого итератора, AFAIK, линейно по размеру карты; поэтому я написал собственный итератор, работающий с абстрактным «контейнером», который «держит» длину последовательности всех возможных чисел (но на самом деле вычисляет их и кэширует только по мере необходимости). По сути, это то же самое решение unordered_map, только сверху добавлен дополнительный уровень итераторов. Решение не укладывалось в 3 сек. предел.

И теперь приходит то, что я не могу понять. Хотя очевидно, что третье решение намеренно усложнено, я с трудом мог поверить, что дополнительный слой итераторов может привести к такому замедлению. Чтобы проверить это, я добавил тот же слой итератора в решение vector. Это моё четвертое решение. Судя по тому, как эта идея с итератором повлияла на мое решение unordered_map, здесь я тоже ожидал значительного замедления; но как ни странно, этого не произошло вовсе. Это решение работает почти так же быстро, как и обычное vector, за 0,035 с.

Как это возможно? Что именно отвечает за замедление в третьем решении? Как возможно, что чрезмерное усложнение двух одинаковых решений совершенно одинаковым образом очень сильно замедляет одно из них и почти не вредит другому? Почему добавление слоя итератора к решению unordered_map привело к тому, что оно не подошло по времени, а то же самое к решению vector почти не замедлило его?

РЕДАКТИРОВАТЬ:

Я обнаружил, что проблема кажется наиболее заметной, если ввод содержит много повторяющихся строк. Я протестировал все четыре решения на своей машине против ввода 1 1000000, повторив 200 раз. Решение с простым вектором обработало их все за 1,531 с. Решение с вектором и дополнительным слоем итератора заняло 3,087 сек. Решение с простой неупорядоченной картой заняло 33,821 с. А решение с неупорядоченной картой и дополнительным слоем итератора заняло более полчаса — я остановил его через 31 минуту 0,482 секунды! Я протестировал его на Linux mint 17.2 64 бит, версия g++ Ubuntu 4.8.4-2ubuntu1~14.04 с флагами -std=c++11 -O2, процессор Celeron 2955U @1,4 ГГц x 2


person Community    schedule 22.07.2015    source источник
comment
Я не могу воспроизвести проблему. Третье решение завершается на моей машине за незначительное время для данной программы и ввода. (time сообщает 1-2 мс.) То же самое для второго. Возможно, опубликовать более сложный входной файл?   -  person Potatoswatter    schedule 22.07.2015
comment
… изменение последней строки на 900 1000000 увеличивает время до диапазона 1 секунды: третье решение выполняется за 1 секунду, а второе — за 2 секунды. Не кажется слишком чувствительным к уровню оптимизации или GCC против Clang.   -  person Potatoswatter    schedule 22.07.2015
comment
@Potatoswatter UVA работает следующим образом: вы предоставляете им исходный файл, они компилируют его и запускают на своей машине, загружая вашу программу своими входными данными, что обычно довольно сложно. Если ваша программа не завершается в установленные ими сроки (здесь 3 секунды), они расценивают ее как неправильную. К сожалению, они держат свой вклад в секрете, поэтому я не могу опубликовать его здесь. Они публикуют некоторые образцы входных данных — это то, что я разместил здесь, — но обычно они настолько легковесны, что вряд ли подходят для какого-либо тестирования. Время, которое я опубликовал, - это время, когда решения работали на их сервере и их вводе.   -  person    schedule 22.07.2015
comment
@Potatoswatter Я запускаю программу на своей машине против ввода 1 1000000. Результаты: Решение с простым vector: 0,046 с; Решение с vector и итераторами: 0,064 с; Решение с unordered_map: 1,26 с; И решение с unordered_map и дополнительным слоем итератора: 2,656 сек. Кстати, вы уверены, что в своем вводе третье решение завершено быстрее, чем второе? Это было бы интересно... как добавление этих итераторов может ускорить время выполнения?   -  person    schedule 22.07.2015
comment
Да, я уверен, что решение №2 (только unordered_map) в два раза быстрее решения №3 (плюс lengthsbase::iterator) на моей машине. Я не знаю почему. Я использую GCC 5.1 и Clang 3.6 в OS X на Macbook с процессором Haswell 2,3 ГГц Core i7. В любом случае, даже если ваши цифры 2:1 в другом направлении, это все равно совершенно другой результат, в котором нет неожиданности того, что сказал онлайн-судья.   -  person Potatoswatter    schedule 22.07.2015
comment
Моя точка зрения не столько о том, какие входные данные используются онлайн-судьей, сколько о том, что, по вашему мнению, мы должны использовать здесь, на SO. Лучше не заставлять каждого отвечающего разрабатывать свой собственный тест.   -  person Potatoswatter    schedule 22.07.2015
comment
Eek, тот другой комментарий был задом наперед. Не вдвое быстрее, а вдвое дольше. Я дважды проверил. Серьезно.   -  person Potatoswatter    schedule 22.07.2015
comment
@Potatoswatter Я проверил все четыре решения на вводе 1 1000000, повторенном 200 раз. Обычный вектор: 1,531 с; вектор с итераторами: 3,087 сек; обычная неупорядоченная карта: 33,821 с; неупорядоченная карта с итераторами: не знаю, остановил ее через 31 минуту 0,482 сек. Linux mint 17.2 64 бит, версия g++ Ubuntu 4.8.4-2ubuntu1~14.04 с флагами -std=c++11 -O2, процессор Celeron 2955U @1,4 ГГц x 2   -  person    schedule 22.07.2015


Ответы (1)


Это кажется проблемой в GCC 4.8. Это не происходит в 4.9 вверх. По какой-то причине последующие внешние циклы (с заполненным кешем unordered_map) выполняются все медленнее, а не быстрее. Я не знаю, почему, так как unordered_map не становится больше.

Если вы перейдете по этой ссылке и переключите GCC 4.8 на 4.9, вы увидите ожидаемое поведение, когда последующие запуски с тем же числовым диапазоном добавляют мало времени, поскольку они используют кеш.

В целом, философия "консервативности" при обновлении компилятора давно устарела. Компиляторы сегодня тщательно тестируются, и вы должны использовать последнюю (или, по крайней мере, недавнюю) версию для повседневной разработки.

Для онлайн-судьи подвергать вас давно исправленным ошибкам просто жестоко.

person Potatoswatter    schedule 23.07.2015
comment
Большое спасибо. Действительно, сам судья говорит, что использует *4.8.2 — компилятор GNU C++ с параметрами: -lm -lcrypt -O2 -std=c++11 -pipe -DONLINE_JUDGE для C++11. Что касается меня, я просто использую стандартную версию из стандартных репозиториев... Боюсь, мне пришлось бы включить бэкпорты, если бы я обновил свой компилятор... - person ; 23.07.2015
comment
Также посмотрите этот тест, который я сделал: melpon.org/wandbox/permlink/XRocZilmeRupsDk9 Здесь у нас есть обратная ситуация: gcc 4.8.2 создает программу, которая выполняется за ~7 секунд, а gcc 4.9.0 и even gcc 5.2.0 и even gcc 6.0.0 создает программу, которую Wandbox убивает из-за слишком долгой работы. Эталонный тест чем-то похож на проблему, обсуждаемую здесь. - person ; 23.07.2015
comment
Кстати, Clang 3.6.0, похоже, отлично справляется с обеими ссылками Wandbox. - person ; 23.07.2015