Maximálně negativní

Když už jsem v tom, tak bych po minulém článku, tu mohl zmínit dvě drobnosti, které se nesou v podobném duchu. Jedna je jakž takž užitečná a druhá je jen intelektuální kuriozita.

#1

Nějakou dobu jsem ve Scale při řazení podle intů používal konstrukci

xs.sortBy(i => -i)

jako zkratku pro

xs.sorted.reverse

To je pěkně kompaktní, potenciálně efektivnější (nevytváří se jedna dočasná kolekce) a hlavně je to špatně.

Koho napadne, co může být špatně na jednom jediném znaku?

Problém je v tom, že znaménkové inty mají o jednu negativní hodnotu víc než těch pozitivních. Proto platí, že

Int.MinValue == -Int.MinValue

math.abs(Int.MinValue) == Int.MinValue

Z toho plyne, že když řadím podle -i, Int.MinValue bude chybně zařazený na začátek. Naštěstí existuje jednoduché řešení, které má taky jen jeden znak:

xs.sortBy(i => ~i)

Kdyby rozdíl nebyl vidět, místo mínusu je tilda, místo číselné negace je bitová negace. Pro tu platí, že

~Int.MinValue == Int.MaxValue
~0 = -1
~1 = -2

a všechno je zase v pořádku.

#2

Nedávno jsem přemýšlel nad následujícím problémem: Je možné porovnat dva znaménkové inty a dostat výsledek jako jeden bit bez CMP a SETE x86 instrukcí?

S CMP a SETE instrukcemi je to brnkačka. CMP porovná dvě hodnoty a nastaví příznaky do stavového registru. SETE pak extrahuje bit označující výsledek rovnosti. Ale jde to i bez nich?

Překvapivě ano.

Rovnost a == b je možné zapsat jako

a >= b & a <= b

Když tohle platí, a a b si musí být rovny. Výraz je pochopitelně rovný tomuhle:

!(a<b) & !(a>b)

Jestliže a < b, tak platí, že b-a je negativní a má tedy nejvyšší bit nastavený na jedničku. Můžu tedy nerovnost přepsat jako

~((b-a) >>> 31) & ~((a-b) >>> 31)

protože všechny operace jsou bitově paralelní, můžu výraz zjednodušit na

(~(b-a) & ~(a-b)) >>> 31

a pak na to hodit De Morgana

~((b-a) | (a-b)) >>> 31

Pět operací, to není špatné.

Za pozornost stojí, že obě nerovnosti můžou přetéct a vrátit špatný bit. Nikdy se však nestane, že přeteče jenom jedna z nich a zkazí výsledek. Obě nerovnosti vždy přetékají v tandemu. Když jedna přeteče a zkazí výsledek, druhá přeteče taky a opraví ho.

Teď jen přijít na to, kde by se to dalo použít.

Co se týká rychlosti, nativní CMP+SETE je pochopitelně rychlejší.


Když už jsem v tom, měl bych zmínit pár paperů, které přišly s užitečnými bit twidlling hacky.

Prvním je Faster Population Counts using AVX2 Instructions, který překonává dřívější SSSE3 fast popcount. Chytrým využitím SIMD instrukcí a bitově paralelního kódu je možné napsat population count, který je 2× rychlejší než nativní popcnt instrukce.

Dalším je Fast Sorted-Set Intersection using SIMD Instructions, který ukazuje jak napsat rychlé sjednocování množin za pomocí nejCISCovatější ze všech x86 CISC instrukci PCMPESTRM (hodí se to např. pro Jaccardovu podobnost)

A nakonec pak Sorting improves word-aligned bitmap indexes, což je tour de force publikace o (komprimovaných) bitových indexech.

To je všechno, rozejděte se.


Dále k tématu:

Flattr this!

This entry was posted in CPU, low level. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *