?

Log in

No account? Create an account

Previous Entry | Next Entry

Подбор паролей на D

Увидел на днях у 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'ов (индентификаторов потоков), запустив нужное число воркеров, а потом раскидывает им задания:

workers[i % $].send( Part( pass.idup, b ) );


$ внутри [] означает длину массива, так номер задачки i мапится на номер рабочего. Встречающиеся в коде .idup и .dup создают иммутабельную и мутабельную копии массива соответственно (посылать между потоками без глубокого копирования разрешается лишь иммутабельные данные).

У каждого потока свой почтовый ящик, откуда он полученные сообщения достает функцией receive, передавая ей разные функции-обработчики, receive по типу этих обработчиков выбирает подходящий, кому отдать данные из сообщения. Соответственно, главный цикл рабочего потока, по мотивам гошного варианта, выглядит
while(true)	
  receive( (Part p) { 
    ... тут вложенная функция, вызывающаяся при получении структуры Part


Как и в гошном варианте, когда главный поток получает сообщение с ответом, он его выводит и завершается. Как я понял, в Go остальные потоки при этом тут же умирают в мучениях. В D при завершении главного потока потоки-воркеры продолжают свою работу. При таком бесконечном цикле ожидания сообщений, как здесь, в какой-то момент мэйлбокс потока оказывается пуст, и в этот момент если receive видит, что родительский поток помер, эта функция бросает определенное исключение. На него можно при желании осмысленно реагировать, но в моем случае я просто говорю тихонько завершиться:
scope(failure) return;

Такое вот баловство.

Tags:

Comments

Yauheni Akhotnikau
Apr. 10th, 2015 08:25 am (UTC)
Придумался такой сценарий. Есть N тасков (где N > 10, чило можно подобрать). Каждый таск -- это md5-хеш и диапазон возможных значений. Например {"c4ca4238a0b923820dcc509a6f75849b", ["0","9")} или {"698d51a19d8a121ce581499d7b701668", ["000","zzz")}.
Менеджер раздает таски воркерам когда воркеры освобождаются. Воркер получает таск, пытается подобрать значение из указанного в таске диапазона, которое даст такой же md5-хеш. Такое значение может быть, а может и не быть. О результате воркер сообщает менеджеру. Получение результата менеджером означает, что воркер освободился и может получить следующий таск (если таковые еще есть).

Диапазоны могут быть заданы некорректно. Например, ["zzz", "000") -- нарушение правила start меньше end. Или ["0000", "zz") -- в end меньше символов, чем в start. Или ["aaaa", "aaAz") -- символ A не входит в разрешенный алфавит.

Столкнувшись с некорректным диапазоном воркер должен "упасть". Эту ситуацию должен отловить менеджер и запустить вместо упавшего воркера нового. Сбойный таск при этом выбрасывается.

Вроде должно получиться не слишком сложнее исходной задачки. При этом это и не чисто на параллельные вычисления, и не чисто на многозадачность. Ну и как-то ближе к реальности. Список тасков можно сформировать и зафиксировать для использования одинакового для всех реализаций.
swizard
Apr. 10th, 2015 12:10 pm (UTC)
Ага, вроде получилась интересная постановка задачи, готов попробовать изобразить на rust.

Я бы тогда ещё чуток модифицировал:
  • чтобы программа интервалы получала на stdin, и запуск чтоб производился как % ./program < intervals.txt
  • чтоб на stderr программа логгировала некорректные интервалы с причиной ошибки из мастер-потока
Yauheni Akhotnikau
Apr. 10th, 2015 12:34 pm (UTC)
А вот читать stdin нужно сразу при старте, или в процессе работы?

Если будет чтение из потока, то нужно тогда использовать совсем простой формат, чтобы не усложнять код примера парсингом и проверками.

На счет вывода в stderr согласен.
swizard
Apr. 10th, 2015 01:56 pm (UTC)
Там данные вполне простые, можно что-нибудь вроде "<md5><пробел><начало интервала><пробел><конец интервала><\n>" на каждой строке.

Чтоб можно было просто сделать сплит по пробелам и нормально (соответственно, пробел и перенос строки не должны входить в алфавит).

Edited at 2015-04-10 01:57 pm (UTC)
Yauheni Akhotnikau
Apr. 10th, 2015 02:04 pm (UTC)
Ok. Не уверен, что смогу заняться завтра. Скорее это будет понедельник-вторник на следующей неделе.
swizard
Apr. 10th, 2015 02:56 pm (UTC)
Окей, а я, видимо, сегодня вечерком что-нибудь накидаю.
Yauheni Akhotnikau
Apr. 11th, 2015 07:37 am (UTC)
Предлагаю, для простоты, диапазоны для поиска задавать так, чтобы и левая и правая граница входили в поиск. Т.е. в виде [l,r], а не [l,r).

В этом случае проще задавать файлы заданий:

95ebc3c7b3b9f1d2c40fec14415d3cb8 yyyyy zzzzz

Вместо

95ebc3c7b3b9f1d2c40fec14415d3cb8 yyyyy 100000
swizard
Apr. 11th, 2015 11:40 am (UTC)
ага, согласен