Подбор паролей на D
Apr. 9th, 2015 07:17 pmУвидел на днях у afiskon'a гостевой пост с реализацией на Go игрушечной задачки про подбор пароля по его MD5 хэшу. По условию в пароле 5 символов из алфавита 0..9a..z, перебор надо распараллелить по ядрам. Там же есть ссылки на предыдущие инкарнации (на хаскеле и эрланге), в них я сейчас не вчитывался. А еще тот же пост увидел eao197 и сделал свой вариант на C++ с использованием его любимого SObjectizer'a. Я решил присоединиться к флэшмобу и сделал вариант на D. Варианты на С++ и D - прямые переводы с Го, поэтому за объяснением рекомендую смотреть пост про гошный вариант, там подробно расписано. Полные исходники:
Go (84 lines)
C++ (155 lines)
D (48 lines)
На моем ноуте с i3 и двумя ядрами по два гипертреда вариант на Go отрабатывает за 13 секунд, вариант на D - за 6 секунд. Оба грузят проц под 100%, имеют по 5 потоков и памяти едят меньше 2 мегов. Что занятно, на этом примере получился очень резкий контраст между компиляторами D: при сборке LDC получается вдвое быстрее Го, а при сборке DMD - вдвое медленнее.
Что касается кода, в варианте на С++ просто почерк очень размашистый, при другом форматировании было бы заметно компактнее, но и синтаксического шума, конечно, тоже заметно больше чем в остальных вариантах. В Го рабочие потоки получают задания и посылают ответ через каналы, которые они шарят меж собой. В D использован модуль из стандартной библиотеки std.concurrency, где нет в явном виде каналов, а вместо этого потоки могут посылать друг другу сообщения аки процессы в эрланге. В частности, у меня тут главный поток создает массив Tid'ов (индентификаторов потоков), запустив нужное число воркеров, а потом раскидывает им задания:
$ внутри [] означает длину массива, так номер задачки i мапится на номер рабочего. Встречающиеся в коде .idup и .dup создают иммутабельную и мутабельную копии массива соответственно (посылать между потоками без глубокого копирования разрешается лишь иммутабельные данные).
У каждого потока свой почтовый ящик, откуда он полученные сообщения достает функцией receive, передавая ей разные функции-обработчики, receive по типу этих обработчиков выбирает подходящий, кому отдать данные из сообщения. Соответственно, главный цикл рабочего потока, по мотивам гошного варианта, выглядит
Как и в гошном варианте, когда главный поток получает сообщение с ответом, он его выводит и завершается. Как я понял, в Go остальные потоки при этом тут же умирают в мучениях. В D при завершении главного потока потоки-воркеры продолжают свою работу. При таком бесконечном цикле ожидания сообщений, как здесь, в какой-то момент мэйлбокс потока оказывается пуст, и в этот момент если receive видит, что родительский поток помер, эта функция бросает определенное исключение. На него можно при желании осмысленно реагировать, но в моем случае я просто говорю тихонько завершиться:
Такое вот баловство.
Go (84 lines)
C++ (155 lines)
D (48 lines)
На моем ноуте с i3 и двумя ядрами по два гипертреда вариант на Go отрабатывает за 13 секунд, вариант на D - за 6 секунд. Оба грузят проц под 100%, имеют по 5 потоков и памяти едят меньше 2 мегов. Что занятно, на этом примере получился очень резкий контраст между компиляторами D: при сборке LDC получается вдвое быстрее Го, а при сборке DMD - вдвое медленнее.
Что касается кода, в варианте на С++ просто почерк очень размашистый, при другом форматировании было бы заметно компактнее, но и синтаксического шума, конечно, тоже заметно больше чем в остальных вариантах. В Го рабочие потоки получают задания и посылают ответ через каналы, которые они шарят меж собой. В D использован модуль из стандартной библиотеки std.concurrency, где нет в явном виде каналов, а вместо этого потоки могут посылать друг другу сообщения аки процессы в эрланге. В частности, у меня тут главный поток создает массив Tid'ов (индентификаторов потоков), запустив нужное число воркеров, а потом раскидывает им задания:
workers[i % $].send( Part( pass.idup, b ) );
$ внутри [] означает длину массива, так номер задачки i мапится на номер рабочего. Встречающиеся в коде .idup и .dup создают иммутабельную и мутабельную копии массива соответственно (посылать между потоками без глубокого копирования разрешается лишь иммутабельные данные).
У каждого потока свой почтовый ящик, откуда он полученные сообщения достает функцией receive, передавая ей разные функции-обработчики, receive по типу этих обработчиков выбирает подходящий, кому отдать данные из сообщения. Соответственно, главный цикл рабочего потока, по мотивам гошного варианта, выглядит
while(true)
receive( (Part p) {
... тут вложенная функция, вызывающаяся при получении структуры PartКак и в гошном варианте, когда главный поток получает сообщение с ответом, он его выводит и завершается. Как я понял, в Go остальные потоки при этом тут же умирают в мучениях. В D при завершении главного потока потоки-воркеры продолжают свою работу. При таком бесконечном цикле ожидания сообщений, как здесь, в какой-то момент мэйлбокс потока оказывается пуст, и в этот момент если receive видит, что родительский поток помер, эта функция бросает определенное исключение. На него можно при желании осмысленно реагировать, но в моем случае я просто говорю тихонько завершиться:
scope(failure) return;
Такое вот баловство.
no subject
Date: 2015-04-09 05:35 pm (UTC)70 строк, но по-сути один хрен, я придерживался стиля, но можно и сжать, это непринципиально. На моём ноуте получается 4.5 сек на подбор.
Но, вообще, я бы изменил бенчмарк -- в этом виде он мало чего показывает. Например, можно было бы не интервалы пересылать, а каждый пароль отдельно (чтобы измерить производительность каналов), ну или убрать требование про каналы, чтобы получить максимальную производительность кода.
no subject
Date: 2015-04-09 06:11 pm (UTC)no subject
Date: 2015-04-13 03:56 pm (UTC)И тут мог бы победить тот, кто сделает векторную MD5. Или, даже, bit sliced SSE-vectorized MD5.
no subject
Date: 2015-04-09 06:30 pm (UTC)Можно было бы вернуться "к истокам" и попробовать пойти в другом направлении: резко увеличить количество диапазонов (например, чтобы диапазон определялся не по порядковому номеру цифры, а по количеству значений в нем -- скажем, по 5 или 10 тысяч). Затем сделать обработку ситуации, когда пароль не найден. Сейчас-то все программы просто зависнут, что не есть правильно. Затем еще вот такое ограничение: как только пароль найден, все рабочие нити должны завершиться при окончании обработки своего текущего блока (либо они новые блоки не запрашивают, либо же им не отвечают, либо же им отсылается специальное значение, запрещающее работать).
Тогда этот пример сможет быть показателем гибкости удобства использования инструмента не с точки зрения скорости конкретной реализации md5, а с точки зрения написания и сопровождения кода.
no subject
Date: 2015-04-09 07:54 pm (UTC)no subject
Date: 2015-04-09 07:58 pm (UTC)no subject
Date: 2015-04-09 08:59 pm (UTC)no subject
Date: 2015-04-09 09:01 pm (UTC)no subject
Date: 2015-04-10 04:29 am (UTC)no subject
Date: 2015-04-10 08:25 am (UTC)Менеджер раздает таски воркерам когда воркеры освобождаются. Воркер получает таск, пытается подобрать значение из указанного в таске диапазона, которое даст такой же md5-хеш. Такое значение может быть, а может и не быть. О результате воркер сообщает менеджеру. Получение результата менеджером означает, что воркер освободился и может получить следующий таск (если таковые еще есть).
Диапазоны могут быть заданы некорректно. Например, ["zzz", "000") -- нарушение правила start меньше end. Или ["0000", "zz") -- в end меньше символов, чем в start. Или ["aaaa", "aaAz") -- символ A не входит в разрешенный алфавит.
Столкнувшись с некорректным диапазоном воркер должен "упасть". Эту ситуацию должен отловить менеджер и запустить вместо упавшего воркера нового. Сбойный таск при этом выбрасывается.
Вроде должно получиться не слишком сложнее исходной задачки. При этом это и не чисто на параллельные вычисления, и не чисто на многозадачность. Ну и как-то ближе к реальности. Список тасков можно сформировать и зафиксировать для использования одинакового для всех реализаций.
no subject
Date: 2015-04-10 12:10 pm (UTC)Я бы тогда ещё чуток модифицировал:
no subject
Date: 2015-04-10 12:34 pm (UTC)Если будет чтение из потока, то нужно тогда использовать совсем простой формат, чтобы не усложнять код примера парсингом и проверками.
На счет вывода в stderr согласен.
no subject
Date: 2015-04-10 01:56 pm (UTC)Чтоб можно было просто сделать сплит по пробелам и нормально (соответственно, пробел и перенос строки не должны входить в алфавит).
no subject
Date: 2015-04-10 02:04 pm (UTC)no subject
Date: 2015-04-10 02:56 pm (UTC)no subject
Date: 2015-04-11 07:37 am (UTC)В этом случае проще задавать файлы заданий:
95ebc3c7b3b9f1d2c40fec14415d3cb8 yyyyy zzzzz
Вместо
95ebc3c7b3b9f1d2c40fec14415d3cb8 yyyyy 100000
no subject
Date: 2015-04-11 11:40 am (UTC)no subject
Date: 2015-04-10 01:32 am (UTC)no subject
Date: 2015-04-10 02:56 am (UTC)На одной стандартной библиотеке нельзя выехать?
Попробовал с такими зависимостями:
[dependencies]
rust-crypto = "0.2.31"
sys-info = "0.3.3"
rustc-serialize = "0.3.12"
Но один из пакетов, от которых они зависят, не собрался, чего-то в моем mingw не хватило.
no subject
Date: 2015-04-10 12:00 pm (UTC)[dependencies]
rust-crypto = "*"
rustc-serialize = "*"
sys-info = "*"
Что касается выехать на стандартной библиотеке -- уже нет :) Там щас новая политика партии: всё по-максимуму вынести из stdlib в crates.io. Вот эти crypto, serialize, и даже rand -- всё это раньше было в стандартной библиотеке.
Не сказал бы, что меня радует такой порядок вещей, но сейчас вот так вот.