Чт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 операций на нём невозможно!
(Придётся халтурить и написать
генератор дрожжевого кода в самом симуляторе при загрузке страницы, чтобы все громоздкие функции генериловались автоматически, не засоряя основной листинг симулятора…)
Да, в интернете очень много профессиональных отладчиков, среди которых самый простой и удобный -
бразильский: Шустро загружается, интерфейс без излишеств, легко запустить отладку.
Но ни в каких отладчиках я не видел функции
отката во времени: Если инструкция выполнилась, нельзя узнать про состояния регистров до её выполнения, иначе как перезапустить весь процесс с самого начала!
Так и родилась идея данного симулятора, который журналирует исполнение каждой инструкции на каждой строчке и во множестве итераций цикла.
![Пишу](images/smilies/write.gif)