Оптимизация майнинга лайткоинов

в 13:17, , рубрики: gpgpu, litecoin, дневник, оптимизация, Песочница, метки: , ,

Всем привет! Я решил рассказать вам о том, как оптимизировал алгоритм майнинга лайткоинов. А представлю я свой рассказ в форме дневника.

День 0: Наткнулся на топик, где некий bitless поделился с сообществом способом ускорить майнинг на несколько процентов. Спросил себя: чем я хуже него? Приступил к анализу кода.
День 1: Вспоминаю синтаксис, нахожу информацию о функциях OpenCL'а.
День 2: Наткнулся на очень странные строчки кода:

#define Coord(x,y,z) x+y*(x ## SIZE)+z*(y ## SIZE)*(x ## SIZE)
#define CO Coord(z,x,y)

Подставил первую сточку во вторую, получил

#define CO z+x*zSIZE+y*xSIZE*zSIZE

Зачем было городить огород — мне не понятно, к тому-же в функции scrypt_core нашлась неиспользуемая переменная ySIZE.
Так же в функции search нашел многократные использования i*2, заменил на rotl(i,1U). Прироста производительности это не дало, но пусть будет.

День 3: Понял, что мой единственный шанс что-то оптимизировать, при текущем уровне знаний — помочь компилятору с «CO», ведь при настройках по умолчанию, z+x*zSIZE+y*xSIZE*zSIZE считается 8704 раза. Скорее всего, компилятор как-то оптимизирует эти вычисления, но это не мешает ему немножко помочь :) К тому-же zSIZE — константа, равная 8, а xSIZE — константа, получаемая из настроек программы.
Начал исследовать первый цикл, в котором используется «CO»:

for(uint y=0; y<1024/LOOKUP_GAP; ++y)
	{
#pragma unroll
		for(uint z=0; z<zSIZE; ++z)
			lookup[CO] = X[z];
		//далее неинтересный мне код
	}

Видно, что x*zSIZE — константа, поскольку x не меняется в ходе выполнения цикла. Так же очевидно, что y увеличивается на 1 с каждой итерацией цикла.

Понимая это, напрашивается создание переменной CO, в которой изначально будет хранится x*zSIZE, а с каждой итерацией цикла, в нее будет добавляться xSIZE*zSIZE.

А, чтобы переменная z нам не мешала, создадим внутри цикла локальную переменную, к которой и будем прибавлять по единице после каждой итерации внутреннего цикла.

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

В итоге получился следующий код:

uint CO=rotl(x,3U);// x*zSIZE
	uint CO_tmp=rotl(xSIZE,3U);//xSIZE*zSIZE

	for(uint y=0; y<1024/LOOKUP_GAP; ++y, CO+=CO_tmp)
	{
		uint CO_reg=CO;
#pragma unroll
		for(uint z=0; z<zSIZE; ++z, ++CO_reg)
			lookup[CO_reg] = X[z];
		//далее неинтересный мне код
	}

День 4: Анализирую следующие использования «CO». Код в препроцессоре пропускаю, поскольку там оно используется только один раз.

for (uint i=0; i<1024; ++i) 
	{
		uint4 V[8];
		uint j = X[7].x & K[85];//K[85]=0x000003FFU
		uint y = (j/LOOKUP_GAP);
#pragma unroll
		for(uint z=0; z<zSIZE; ++z)
			V[z] = lookup[CO];
		//далее неинтересный мне код
	}

Видно, что y меняется по довольно сложному алгоритму, предугадать значение этой переменной получится вряд ли.
Поэтому все, что я могу — посчитать заранее x*zSIZE и xSIZE*zSIZE.

CO_tmp=rotl(x,3U);
	CO=rotl(xSIZE,3U);
	for (uint i=0; i<1024; ++i) 
	{
		uint4 V[8];
		uint j = X[7].x & K[85];
		uint y = (j/LOOKUP_GAP);
		uint CO_reg=CO_tmp+CO*y;
		
		for(uint z=0; z<zSIZE; ++z, ++CO_reg)
			V[z] = lookup[CO_reg];
		//далее неинтересный мне код
	}

День 5: Компилирую, и наблюдаю прирост на своей конфигурации ~3%. Перепроверив результаты несколько раз, привожу код в человеческий вид, оптимизирую код в препроцессорном участке так же, как во втором цикле, и убираю ранее сделанную замену i*2 на rotl(i,1U).

После тестов «очищенного» кода результат меня удивляет — скорость стала существенно меньше, чем до начала моей оптимизации. Проведя небольшое расследование, я выяснил, что причина этому — возвращение обратно i*2, вместо rotl(i,1U).
Казалось-бы, сама замена ничего не дает вообще ничего — проверял несколько раз, однако вместе с моими оптимизациями — увеличивает скорость.

Отправляю результаты моих трудов на почту Con Colivas'у.

День 12: Не дождавшись ответа в течение недели, выкладываю свои достижения и инструкции на официальный форум лайткоинов.
С несколькими людьми это сработало и действительно дало прирост скорости. Однако, в скором времени обнаружилась проблема — я проводил тесты с драйверами 13.4, а с более ранними версиями драйверов(и OpenCl) — скорость падает примерно на треть.
Поставил себе драйвера 13.1 (не без проблем — версия OpenCl понижаться не хотела вплоть до полной очистки системы от драйверов AMD и OpenCL), начинаю исследования.

День 13: Обнаружил, что падение производительности вызывает та самая загадочная замена i*2 на rotl(i,1U). Но, убрав эту замену, скорость возвращается на начальный уровень.

Понимаю, что придется делать две версии оптимизаций: для 13.4, и для более старых версий драйверов.

Дни 13-40: Набрав себе добровольных тестеров, из числа тех, кто сообщал о неработающей оптимизации на 13.1 — начинаю работу.
В ходе тестов обнаруживаются железозависимые оптимизации, от которых я сразу отказываюсь(к таким относится, например, создание массива на 1024 элемента, в которых хранятся предпосчитанные значения y*xSIZE*zSIZE, для y=0..1023 — у меня оптимизация заработала, у тестеров нет), а так же нелинейное влияние частот на результаты некоторых оптимизаций: на моей 7850 при частотах 1000/1300, выдавались аномальные результаты, вроде ~340 килохешей в секунду при интенсивности 13(позволяет спокойно работать за компьютером, без подлагиваний в то время, когда видеокарта майнит), вместо ~200 килохешей в секунду без моей оптимизации и/или на других частотах.

Но, от таких оптимизаций пришлось отказаться(почти, для себя эту версию я сохранил).

Так же обнаружилось «магическое значение» настройки --thread-concurency, при которой скорость растет, равное 2^n+1, например 4097 или 16385. При любом другом значении скорость майнинга меньше.

Мои предположения — возможно, что умножение на 2^n+1 выполняется быстрее всего, но это не совсем логично, ведь умножение на 2^n — это простой битовый сдвиг влево, и, по идее, должен выполняться быстрее…

В итоге я пришел к следующему коду:

uint CO_tmp=xSIZE<<3U;//xSIZE*zSIZE
	uint CO_tmp2=x<<3U;//x*zSIZE

	for(uint y=0; y<1024/LOOKUP_GAP; ++y)
	{
		uint CO=y*CO_tmp+CO_tmp2;
#pragma unroll
		for(uint z=0; z<zSIZE; ++z,++CO)
			lookup[CO] = X[z];
		//далее неинтересный мне код
	}
	
	//неизменный код
	
	for (uint i=0; i<1024; ++i) 
	{
		uint4 V[8];
		uint j = X[7].x & K[85];
		uint y = (j/LOOKUP_GAP);
		uint CO=y*CO_tmp+CO_tmp2;
#pragma unroll
		for(uint z=0; z<zSIZE; ++z)
			V[z] = lookup[CO+z];
		//далее неинтересный мне код
	}

Опубликовал версию для драйверов старее, чем 13.4 на том-же форуме, упомянул про «магическое значение» —thread-concurency. И счел свою работу завершенной.

Надеюсь, что знающие люди смогут рассказать мне, что происходит при замене i*2 на rotl(i,1U), а так же природу «магического значения» параметра --thread-concurency.

Мой топик на форуме лайткоинов: forum.litecoin.net/index.php?topic=4082.0

P.S. все работы велись в файле scrypt.cl, входящем в комплект к любому майнеру, поддерживающему майнинг лайткоинов.

Автор: Bazanra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js