Показать сообщение отдельно
  #5 (permalink)  
Старый 26.07.2022, 22:22
Аватар для Alikberov
Кандидат Javascript-наук
Отправить личное сообщение для Alikberov Посмотреть профиль Найти все сообщения от Alikberov
 
Регистрация: 16.08.2018
Сообщений: 112

Немного теории для введения в SIMD-симуляцию
Чтoбы понять все сложности и подводные камни технологии эмуляции SIMD-расчётов, рассмотрим несколько простейших операций.

Сутью SIMD-механизма является т.н. горизонтальное вычисление, когда одной операцией обрабатывается несколько данных, упакованных в одно слово.

Например, сравним две функции сложения упакованных данных.
Так как несущее слово имеет разрядность в 64 бита, то и дробление всех действий кратно длине упакованных данных в нём соответственно.
Если данные - 8-битные слова, то в общем 64-битном умещается их восемь и функция выходит довольно длинной и скучной:
// Сложение восьми 8-битных слов, упакованных в одно 64-битное слово
BigInt.prototype.PADDB = function(n) {
	//	Packed Add for Bytes
	var	d1 = 0xFFn & this;			// Распаковываем из 64-битного слова
	var	d2 = 0xFFn & (this >> 8n);		// восемь отдельных 8-битных
	var	d3 = 0xFFn & (this >> 16n);
	var	d4 = 0xFFn & (this >> 24n);
	var	d5 = 0xFFn & (this >> 32n);
	var	d6 = 0xFFn & (this >> 40n);
	var	d7 = 0xFFn & (this >> 48n);
	var	d8 = 0xFFn & (this >> 56n);
	var	n1 = 0xFFn & n;				// Распаковываем из 64-битного слова
	var	n2 = 0xFFn & (n >> 8n);			// восемь отдельных 8-битных
	var	n3 = 0xFFn & (n >> 16n);
	var	n4 = 0xFFn & (n >> 24n);
	var	n5 = 0xFFn & (n >> 32n);
	var	n6 = 0xFFn & (n >> 40n);
	var	n7 = 0xFFn & (n >> 48n);
	var	n8 = 0xFFn & (n >> 56n);
	var	r1 = ((d1 + n1) & 0xFFn);		// Складываем все восемь 8-битных слов
	var	r2 = ((d2 + n2) & 0xFFn) << 8n;
	var	r3 = ((d3 + n3) & 0xFFn) << 16n;
	var	r4 = ((d4 + n4) & 0xFFn) << 24n;
	var	r5 = ((d5 + n5) & 0xFFn) << 32n;
	var	r6 = ((d6 + n6) & 0xFFn) << 40n;
	var	r7 = ((d7 + n7) & 0xFFn) << 48n;
	var	r8 = ((d8 + n8) & 0xFFn) << 56n;
	return	r8 | r7 | r6 | r5 | r4 | r3 | r2 | r1;	// Упаковываем всё обратно в 64-битное
}
Если же данные - 16-битные слова, то в 64-битном уместится их уже вдвое меньше, соответственно и функция сократится:
// Сложение четырёх 16-битных слов, упакованных в одно 64-битное слово
BigInt.prototype.PADDW = function(n) {
	//	Packed Add for Words
	var	d1 = 0xFFFFn & this;			// Распаковываем из 64-битного слова
	var	d2 = 0xFFFFn & (this >> 16n);		// четыре отдельных 16-битных
	var	d3 = 0xFFFFn & (this >> 32n);
	var	d4 = 0xFFFFn & (this >> 48n);
	var	n1 = 0xFFFFn & n;			// Распаковываем из 64-битного слова
	var	n2 = 0xFFFFn & (n >> 16n);		// четыре отдельных 16-битных
	var	n3 = 0xFFFFn & (n >> 32n);
	var	n4 = 0xFFFFn & (n >> 48n);
	var	r1 = ((d1 + n1) & 0xFFFFn);		// Складываем все четыре 8-битных слов
	var	r2 = ((d2 + n2) & 0xFFFFn) << 16n;
	var	r3 = ((d3 + n3) & 0xFFFFn) << 32n;
	var	r4 = ((d4 + n4) & 0xFFFFn) << 48n;
	return	r4 | r3 | r2 | r1;			// Упаковываем всё обратно в 64-битное
}
Но, как-то всё уныло и скучно получается. Да ещё и циклы применять крайне не желательно. Из-за чего исходный текст программы симуляции растёт как на дрожжах!

Но, есть одна хитрость: Подавить признак переноса из одного разряда в другой в нескольких кратных позициях.
Так, вместо неуклюжих функций выше получаем компактные такие две:
Для 8-битных
BigInt.prototype.PADDB = function(n) {
	return (((this & 0x7F7F7F7F7F7F7F7Fn) + (n & 0x7F7F7F7F7F7F7F7Fn))) ^ ((this ^ n) & 0x8080808080808080n);
}
И для 16-битных
BigInt.prototype.PADDW = function(n) {
	return (((this & 0x7FFF7FFF7FFF7FFFn) + (n & 0x7FFF7FFF7FFF7FFFn))) ^ ((this ^ n) & 0x8000800080008000n);
}
К тому же, всё это хозяйство можно унифицировать до:
BigInt.prototype.PADD = function(n, mask) {
	return (((this & mask) + (n & mask))) ^ ((this ^ n) & ~mask);
}
Только маску подставляй нужную и можно работать с упакованностью любой кратности!

С вычитанием выходит немножечко посложнее, так как там не перенос подавить нужно, а предотвратить займ:
BigInt.prototype.PSUB = function(n, mask) {
	return (((this | ~mask) - (n & mask)) & mask) | ((this - n) & ~mask);
}

Ну, здесь можно добавить сахарку и передавать маску в функцию неявным способом - (мой позорный вопрос №189 посвящён задаче маскировки маски), хотя там тоже свои нюансы.

Если с нормальным сложением/вычитанием упакованных данных как бы всё очевидно, просто и изящно, то с операциями сравнения всё намного хуже!
// Сравниваем восемь 8-битных слов и формируем восемь независимых масок в зависимости от результата сравнения
BigInt.prototype.PCMPEQB = function(n) {
	var	d1 = 0xFFn & this;			// Распаковываем из 64-битного слова
	var	d2 = 0xFFn & (this >> 8n);		// восемь отдельных 8-битных
	var	d3 = 0xFFn & (this >> 16n);
	var	d4 = 0xFFn & (this >> 24n);
	var	d5 = 0xFFn & (this >> 32n);
	var	d6 = 0xFFn & (this >> 40n);
	var	d7 = 0xFFn & (this >> 48n);
	var	d8 = 0xFFn & (this >> 56n);
	var	n1 = 0xFFn & n;				// Распаковываем из 64-битного слова
	var	n2 = 0xFFn & (n >> 8n);			// восемь отдельных 8-битных
	var	n3 = 0xFFn & (n >> 16n);
	var	n4 = 0xFFn & (n >> 24n);
	var	n5 = 0xFFn & (n >> 32n);
	var	n6 = 0xFFn & (n >> 40n);
	var	n7 = 0xFFn & (n >> 48n);
	var	n8 = 0xFFn & (n >> 56n);
	var	r1 = (d1 == n1 ? 0xFFn : 0x00n);	// Сравниваем все восемь 8-битных слов
	var	r2 = (d2 == n2 ? 0xFFn : 0x00n) << 8n;
	var	r3 = (d3 == n3 ? 0xFFn : 0x00n) << 16n;
	var	r4 = (d4 == n4 ? 0xFFn : 0x00n) << 24n;
	var	r5 = (d5 == n5 ? 0xFFn : 0x00n) << 32n;
	var	r6 = (d6 == n6 ? 0xFFn : 0x00n) << 40n;
	var	r7 = (d7 == n7 ? 0xFFn : 0x00n) << 48n;
	var	r8 = (d8 == n8 ? 0xFFn : 0x00n) << 56n;
	return	r8 | r7 | r6 | r5 | r4 | r3 | r2 | r1;	// Упаковываем всё обратно в 64-битное
}
Это просто тихий ужас! Как всё это оптимизировать - ума не приложу…
А ведь это сравнение только 8-битных и только на эквивалентность!

Из-за чего вся моя разработка затягивается на недели или откладывается.

А учитывая, сколько всего команд уста ревшей технологии MMX желательно поддержать в симуляторе, да поглядывая ещё на SSE и 3DNow!, не говоря уж о AVX, то вопрос полной поддержки всего этого не стоит особняком, так как симулятор я разрабатывать начал для личных нужд и поддержку команд ввожу по мере надобности в них.
И меня пока не привлекает тот факт, что BigInt поддерживает и 128, и 256, и 512 бит, так как даже представить весь тот ужас описания AVX-512 операций на нём невозможно!
(Придётся халтурить и написать генератор дрожжевого кода в самом симуляторе при загрузке страницы, чтобы все громоздкие функции генериловались автоматически, не засоряя основной листинг симулятора…)

Да, в интернете очень много профессиональных отладчиков, среди которых самый простой и удобный - бразильский: Шустро загружается, интерфейс без излишеств, легко запустить отладку.
Но ни в каких отладчиках я не видел функции отката во времени: Если инструкция выполнилась, нельзя узнать про состояния регистров до её выполнения, иначе как перезапустить весь процесс с самого начала!
Так и родилась идея данного симулятора, который журналирует исполнение каждой инструкции на каждой строчке и во множестве итераций цикла.

Последний раз редактировалось Alikberov, 26.07.2022 в 22:56.
Ответить с цитированием