Čas od času je potřeba sčítat a odčítat s přetečením/podtečením. Například v případě, že potřebujeme počítat s větší šířkou registrů než máme k dispozici. V assembleru je tato záležitost jednoduchá. Máme k dispozici flag Carry. Ale v C tuhle možnost nemáme. Sice máme možnost napsat část kódu v assembleru, ale to bývá komplikované a nepřenosné.
Nedávno jsem potřeboval napsat 64 bitové sčítání a odčítání pro 32 bitový processor. Přesněji řečeno, celý program umocňuje 64bitové číslo dalším 64bitovým číslem a počítá zbytek po dělení výsledku dalším 64bitovým číslem. Tahle šílenost nakonec vede na sčítání a odčítání 64bitových čísel. Původně jsem to napsal pro i8051, pak pro PIC, nakonec to musím portovat na Atmel, tentokrát v C. A jsme u jádra problému. Jak detekovat přeteční v C.
Idea je celkem jednoduchá. Pokud sčítám dvě kladná čísla, výsledek musí být vyšší nebo roven kterémukoliv ze sčítanců. Pokud ovšem výsledek přeteče, bude menší, než kterýkoliv z nich. Tahle doměnka by se jistě dala dokázat matematicky, ale jednodušší bude ověřit, jestli to funguje pro jakoukoliv kombinaci sčítanců. Ale nebudeme to dělat pro 32bitová čísla, vyzkoušíme to hrubou silou na dvoubitových číslech:
A |
B |
C=A+B |
podmínky |
||||||||
bit1 |
bit0 |
bit1 |
bit0 |
carry |
bit1 |
bit0 |
C<A? |
C<B? |
|||
0 |
0 |
0 |
0 |
0 |
0 |
0 |
NE |
NE |
|||
0 |
0 |
0 |
1 |
0 |
0 |
1 |
NE |
NE |
|||
0 |
0 |
1 |
0 |
0 |
1 |
0 |
NE |
NE |
|||
0 |
0 |
1 |
1 |
0 |
1 |
1 |
NE |
NE |
|||
0 |
1 |
0 |
0 |
0 |
0 |
1 |
NE |
NE |
|||
0 |
1 |
0 |
1 |
0 |
1 |
0 |
NE |
NE |
|||
0 |
1 |
1 |
0 |
0 |
1 |
1 |
NE |
NE |
|||
0 |
1 |
1 |
1 |
1 |
0 |
0 |
ANO |
ANO |
|||
1 |
0 |
0 |
0 |
0 |
1 |
0 |
NE |
NE |
|||
1 |
0 |
0 |
1 |
0 |
1 |
1 |
NE |
NE |
|||
1 |
0 |
1 |
0 |
1 |
0 |
0 |
ANO |
ANO |
|||
1 |
0 |
1 |
1 |
1 |
0 |
1 |
ANO |
ANO |
|||
1 |
1 |
0 |
0 |
0 |
1 |
1 |
NE |
NE |
|||
1 |
1 |
0 |
1 |
1 |
0 |
0 |
ANO |
ANO |
|||
1 |
1 |
1 |
0 |
1 |
0 |
1 |
ANO |
ANO |
|||
1 |
1 |
1 |
1 |
1 |
1 |
0 |
ANO |
ANO |
V tabulce vidíme, že hypotéza skutečně funguje. Kdykoliv, kdy výsledek přeteče, bude C menší než kterýkoliv ze sčítanců. Takže program v C pro sčítání a detekci přetečení může vypadat takto:
unsigned add(unsigned A, unsigned B, bool *CY) { unsigned C=A+B; *CY=C<A; return C; }
Pokud potřebujeme přičíst i předchozí flag přetečení. musíme udělat dva testy.
unsigned addc(unsigned A, unsigned B, bool *CY) { unsigned C=A+B; unsigned local_carry=C<A; if (*CY) { C++; if (C==0) local_carry=true; } *CY=local_carry; return C; }
Já vím, nahradit jedinou instrukci addc programem v C na 12 řádků je perverzní, ale v nenáročných případech je to lepší, protože je to plně přenositelné.
Při odečítání máme situaci jednodušší. Pokud budeme počítat C=A-B a přitom A<B, víme dopředu, že to podteče. Tady nepotřebujeme důkazy.
unsigned sub(unsigned A, unsigned B, bool *Borrow) { *Borrow=A<B; return A-B; } unsigned subb(unsigned A, unsigned B, bool *Borrow) { bool local_borrow=A<B; unsigned C=A-B; if (*Borrow) { if (C==0) local_borrow=true; C--; } *Borrow=local_borrow; return C; }
Taková obrovská tabulka kvůli dvěma ušmudlaným 2bitovým číslům.
No já nevím, kdysi byli "lidé od počítačů" hodně dobře vzděláni v matematice (možná s výjimkou analýzy, tu fakt použijí jenom pro velmi speciální aplikace). Jak se dívám, pro někoho je jednodušší dělat důkazy hrubou silou, a to ještě pro velmi speciální případ, než vzít tužku a papír a na pár řádcích si hypotézu dokázat.
[1,2] V celém zápisku byla snad řeč o bezznaménkových číslech, proč se tam najednou objevují znamenková?
Ona je ovšem tahle tabulka načmáraná na kus papíru podstatně rychlejší a spolehlivější než matematický důkaz. V důkazu totiž můžeš udělat chybu, které si nevšimneš, kdežto když na to jdeš hrubou silou, máš jistotu, že to bude fungovat.
Znaménková čísla tam jsou, ale on ten algoritmus [1] stejně nefunguje.
[2] Protože v mém komentáři jsou otočené zobáčky v porovnávacích operátorech proti tomu, co jsem psal v příspěvku. Tam, kde je mneší, má být větší a naopak.
Bohužel tento redakční systém považuje zobáčky za pokus o HTML značky a tak je mrví.
Takže funkce pro efektivní adc, kterou jsem napsal vyšla s obrácenými zobáčky u menší a větší, než jsem to poslal do redakčního systému.
Lynčujte prosím redaktory root.cz, já za to nemohu.
[3] Znaménková čísla jsem použil proto, že C umí stejné číslo „sekvenci bitů“ operovat různými způsoby.
A někdy je výrazně efektivnější nad stejnou sekvencí bitů (která vstupuje jako bezznaménkový integer) operovat s operacemi pro znaménkový integer. Tak jako v tomto případě.
C je v podstatě velice hloupý jazyk s velice omezenou množinou operací pro bitové operace. Nic moc toho neumí. Chcete-li pracovat efektivně, je třeba využít alespoň plnou palbu toho, co C umí.
A jinak je rozdíl „reprezentace hodnoty čísla“ a „číslo samotné“. Na tom je to založeno.
Miloslav Ponkrác
Ano, přesně tak, vždyť ot musíte vidět a bít to do očí!
První výraz: (A or B) je menší než nula – říká, že k přenosu může dojít jen tehdy, pokud alespoň jedno z čísel A a B má nastavený nejvyšší bit
Druhý výraz: C je nezáporné – má-li A a/nebo B má nejvyšší bit nastavený, pak k přenosu dojde jen tehdy, když výsledné C nemá nejvyší bit nastavený.
To jasně vyplývá z matematických vlastností výsledného oboru hodnot operace sčítání vůči definičnímu oboru.
xxxx
Přidávám se k [3], dnešní programátoři jsou opravdu jen cvičené opice a jejich znalosti jsou nula.
Podle standardu C není výsledek po přetečení definován, což většinou nevadí, protože to dopadne tak, jak to spočítá železo. Problém je, že překladač může eventualitu přetečení ignorovat a některé konstrukce vynechat.
unsigned a, b;
if (a + b < a) {
// tato větev byla eliminována, protože podmínka nemůže být true.
panic();
}
Ovšem tohle nebude fungovat minimálně pro tento případ:
A=0x80, B=0xff a carry=true
vysledek bude C=0x80, v signed je -128, což bude menší než 0
takže
(int)(a | b) < 0 je true
AND
(int)c >= 0 je false
=
carry je false, ale mělo být true
Nemysli si, že jsem nepřemýšlel o tom, jak to udělat jednodušší, ale přičtení současně CY a 0xff dělá problémy.
Asi by to šlo pomocí pár xorů a andů, ale složitostí se to pak už blížilo tomu mému programu na 12 řádků.
A nebo je v tom algoritmu ještě stále někde chyba a nevidím ji.
Neviem ako efektivne robi cpu porovnanie, ale mozno je lepsie ratat s 30 bitmi a pouzit posledny bit na det. pretecenia cez bitove operacie (ktore byvaju rychle), resp napisat si makro v asm a dat to do neprenosnej casti programu. Zase prepisat to neni az taka drina a speedup moze byt znacny.
[15] tohle by asi mohlo fungovat.
rozeberme si to:
carry bude když jsou oba vstupy současně 128 a větší. OK
NEBO
když je aspoň jeden z nich 128 a víc a výsledek je nula.
Ne, to taky nepojede.
a=0xff, b=1, carry=true
výsledek c=1, carry=true
první část podmínky je false, a druhá taky, protože C není 0
ale mělo to být true
[16] porovnávání je relativně rychlé, to není problém. Počítat se 30 bity se nevyplatí, protože potřebuju počítat se 64, tj. dva 32bitové registry. Kdybych to měl rotovat do třech operací po míň jak 32 bitů, bylo by to ještě komplikovanější.
Co se týče rychlosti, na té mi naštěstí tak moc nezáleží. PIC to počítal asi sekundu a to jel po osmi bitech a neměl instrukci addc, takže i on to musel dělat řádově třemi instrukcemi. Tady to pojedu po 32 bitech, sice instrukcí tam bude neúrekom, ale tenhle processor je podstatně rychlejší. A i kdyby ne, klidně ať to počítá 5 sekund.
Celý problém vlastně vniknul tím, že se mi nechtělo hledat jak se dá přistupovat do C proměnných z assembleru :-)
Opravte si redakční systém, aby se dalo normálně psát, zvlášť u programovacího webu, kde nejdou psát ani relace, se s tím těžko pracuje.
To pak 99 % kapacity mozku zabírá zvládnutí babylónských hieroglyfů, které člověk musí psát do redakčního systému. Pak člověk pracuje de facto zpaměti a dělá IQ test kapacity krátkodobé paměti.
Myslím, že směr jsem ukázal.
Heh, ne, díky, já tohle raději přenechám kompileru. :)
http://paste2.org/MydMGtBx
Zvláště v dnešní době, kdy je 64+bit (long long) v C99.
[22]A co když budete potřebovat ještě větší čísla?
Jinak +1 pro řešení v [16]/[21]. Stačí vymaskovat poslední bit a spočítat ho zvlášť. Výsledek je 100% správně a určitě rychlejší než postup v blogu. Protože:
1) Pokud to má být přenositelné, tak se nemůžu spoléhat, že ALU používá dvojkový doplněk.
2) Každý skok a tudíž každý if je výkonostní průšvih.
3) Pokud to uděláte jako makro/inline funkci tak ve výsledku tam bude kupa převážně bezkolizních bitových operací za sebou bez jediného skoku. Superskalární procesor vás za to bude milovat.
[21] Tady není až tak problém spočítat a+b a detekovat carry, tady je problém spočítat a+b+předchozí_carry. Buď to udělám jako 2 sčítání po sobě a pak to bude vypadat plus mínus jako ve článku nebo tam bude 10 testů, které se zatím nepodařilo odladit viz [17]
[23]
1. - pokud sčítám unsigned a přeteče to, nedokážu si představit ALU která by se chovala jinak než jak jsme zvyklí
2,3. naprostý souhlas
[22] to je zajímavý postřeh. Abych byl upřímný, vůbec mě nenapadlo, že long long bude fungovat i na těchto processorech. Sice stejně potřebuju detekovat carry, ale aspoň si ušetřím nutnost sčítat a+b+předchozí_carry
Tak jsem si udělal pokus. To jsem radši neměl dělat. Ztratil jsem veškeré iluze o processoru AVR i o gcc. Jednak jsem si doteď myslel, že AVR je 32 bitů. Je to 8 bitů. Ale jak gcc přeložilo long long sčítání, to je zvrácenost. Chtělo se mi u toho zvracet.
Takže napřed 32 bitové sčítání, abychom si udělali představu:
add32:
.stabd 46,0,0
.stabn 68,0,9,.LM3-.LFBB2
.LM3:
.LFBB2:
/* prologue: function */
/* frame size = 0 */
/* stack size = 0 */
.L__stack_usage = 0
movw r26,r24
movw r24,r22
.stabn 68,0,10,.LM4-.LFBB2
.LM4:
add r18,r24
adc r19,r25
adc r20,r26
adc r21,r27
.stabn 68,0,11,.LM5-.LFBB2
.LM5:
movw r22,r18
movw r24,r20
/* epilogue start */
ret
.
.
.
A teď 64 bitů:
add64:
.stabd 46,0,0
.stabn 68,0,4,.LM0-.LFBB1
.LM0:
.LFBB1:
push r10
push r11
push r12
push r13
push r14
push r15
push r16
push r17
/* prologue: function */
/* frame size = 0 */
/* stack size = 8 */
.L__stack_usage = 8
.stabn 68,0,5,.LM1-.LFBB1
.LM1:
add r18,r10
ldi r26,lo8(1)
cp r18,r10
brlo .L2
ldi r26,lo8(0)
.L2:
mov r30,r11
add r30,r19
ldi r31,lo8(1)
cp r30,r11
brlo .L3
ldi r31,lo8(0)
.L3:
mov r19,r26
add r19,r30
ldi r26,lo8(1)
cp r19,r30
brlo .L4
ldi r26,lo8(0)
.L4:
or r31,r26
mov r26,r12
add r26,r20
ldi r30,lo8(1)
cp r26,r12
brlo .L5
ldi r30,lo8(0)
.L5:
mov r20,r31
add r20,r26
ldi r31,lo8(1)
cp r20,r26
brlo .L6
ldi r31,lo8(0)
.L6:
or r30,r31
mov r26,r13
add r26,r21
ldi r31,lo8(1)
cp r26,r13
brlo .L7
ldi r31,lo8(0)
.L7:
mov r21,r30
add r21,r26
ldi r30,lo8(1)
cp r21,r26
brlo .L8
ldi r30,lo8(0)
.L8:
or r31,r30
mov r26,r14
add r26,r22
ldi r30,lo8(1)
cp r26,r14
brlo .L9
ldi r30,lo8(0)
.L9:
mov r22,r31
add r22,r26
ldi r31,lo8(1)
cp r22,r26
brlo .L10
ldi r31,lo8(0)
.L10:
or r30,r31
mov r26,r15
add r26,r23
ldi r31,lo8(1)
cp r26,r15
brlo .L11
ldi r31,lo8(0)
.L11:
mov r23,r30
add r23,r26
ldi r30,lo8(1)
cp r23,r26
brlo .L12
ldi r30,lo8(0)
.L12:
or r31,r30
mov r26,r16
add r26,r24
ldi r30,lo8(1)
cp r26,r16
brlo .L13
ldi r30,lo8(0)
.L13:
mov r24,r31
add r24,r26
ldi r31,lo8(1)
cp r24,r26
brlo .L14
ldi r31,lo8(0)
.L14:
or r30,r31
add r25,r17
.stabn 68,0,6,.LM2-.LFBB1
.LM2:
add r25,r30
/* epilogue start */
pop r17
pop r16
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
ret
[24] To trochu zavání "na mám počítači to funguje". Jinak historicky se tyhle věci implementovaly právě pomocí nižšího počtu bitů. V tomhle případě bych použil 32bitový int, ale počítal se 16 bity. Pro jeden blok (16 bitů) to pak je:
X = (A + B + carry_in) & 0x0ffff
carry_out = (A + B + carry_in) >> 16 /* shift right, nebo taky AND, záleží zda to použiji v "if" nebo pro aritmetiku */
Pro rozhodnutí, jak velké bloky použít (4, 8, 15, 16, 31 bitů apod.), je důležité vědět, jak máte ta data uložena v paměti. Použití 16 bitů na procesoru co umí 32 (nebo 8 na procesoru co umí 16) je výhodné obvykle proto, že data jsou uložena v paměti jako sled bytů, takže lze pro čtení a zápis použít klasické ukazatele a offsety (pozor na little/big endian). Opticky to vypadá strašidelně, ale procesory to zvládají rychle, protože je to jen sčítání, AND a rotace, navíc hezky proložené čtením a zápisem do paměti, bez skoků. Tedy pokud se nebavíme o funkci _addc_ ale o funkci add_64bit(byte *A, byte *B, carry_in, byte *X, carry_out).
[25] Existuje 8-bit AVR a 32-bit AVR32, ale jsou to dvě naprosto různé věci.
AVR je 8-mi bitové a GCC pro AVR nemá zoptimalizováno long long, protože se to jednoduše nevejde do registrů, AVR má sice 32 registrů, ale 2 až 3 osmibytové operandy se tam z určitých důvodů nevejdou.
Proto se to dělá nějak takhle:
unsigned long long ADC(unsigned long long * a, unsigned long long * b, bool * carry)
{
unsigned long long result;
unsigned char * pr = (unsigned char *) &result;
unsigned char * pa = (unsigned char *) a;
unsigned char * pb = (unsigned char *) b;
unsigned short temp = (carry) ? 1 : 0;
for (int i = 0; i >= 8;
}
*carry = (temp != 0);
return result;
}
(Big endian cpu mě nezajímají, neřeším).
[10][12][26]
Jsem si vcelku jist, ze nedefinovanost se ty tyka pouze signed aritmetiky, preteceni v unsigned aritmetice je definovano minimalne od C99.
Co se tyce realnych optimalizaci v kompilatorech, tak tohle pokud vim dela GCC pri optimalizaci O2 a vyssi. Ma navic volbu -fno-strict-overflow umoznujici toto chovani vypnout pro kompilaci programu, ktere pocitaji se signed overflow. To pouziva napr. Linux kernel.
[10][12][26][31] Ano u znaménkových typů v C je přetečení nedefinovaná operace. Unsigned typy definované jsou.
[12] Bacha, volatile neznamená vypnutí optimalizací. volatile jen zabrání překladači přehazovat a odstraňovat přístupy do paměti.
Z kódu
a = b;
a = 3;
může překladač bez problémů vyhodit to "a=b", takže nenačte "b" a do "a" zapíše jen jednou. Pokud jsou a a b volatile, tak nemůže vyhodit ani to čtení ani ten zápis. Pokud by se s hodnotou "b" něco počítalo, tak to překladač může zoptimalizovat jak chce. Jen nesmí přeskládat přístupy k volatile proměnným.
[7] Pozastavoval jsem nad použitím znaménkového int, protože se důrazně nedoporučuje spoléhat se na vnitřní strukturu znaménkových typů, obzvláště na embedded zařízeních. Myslím že je to dokonce v MISŘE.
Význam výrazu "důrazně se nedoporučuje" by se dal vyjádřit i slovy: "pracky urazit každému, kdo bude dělat se signed int bitové operace".
[7][35]
Konverze na signed integerovy typ, ktery neumoznuje reprezentovat puvodni hodnotu, ma dle C99 implementation-defined chovani. Takze pouzit '(int) c < 0' pro testovani nejvyssiho bitu neni portabilni, ale ani to neni nedefinovane (narozdil od preteceni pri signed aritmetice).
[33] volatile znamená, jak jsi správně napsal, že zabrání překladači přehazovat a ODSTRAŇOVAT přístupy do paměti. To odstraňovat je tam důležité, protože z kódu
a = b;
a = 3;
nemůže překladač vyhodit přiřazení a = b, pokud je proměnná a volatile.
To co jsi popsal (zabránění přeskládání přístupu do paměti) není volatile, ale říká se tomu memory barrier.
[37] Pravda, tohle jsem zapomněl zdůraznit. Uspořádané jsou jen ty volatile přístupy a to jenom z pohledu optimalizací překladače. Ty nevolatile si mezi tím překladač může přehazovat jak chce. Na architekturách jako ARM navíc může hardware přeházet i ty volatilní.
Každopádně od C11 s atomickými operacemi už vlastně ani nevím, k čemu bych volatile použil. S atomic věcmi jde všechno nějak lépe, spolehlivěji a radostněji.
[35] Pracky urážet každému, kdo udržuje při životě obskurdní architektury kde znaménkový integer je něco jiného než dvojkový doplněk. Prostě takový hardware se nekupuje a tyto problémy se neřeší. Obdobným přístupem už se podařilo vymýtit platformy s jinou délkou slova než 2^N a je to tak dobře.
[38] Volatile je něco naprosto jiného jako atomické operace. Jestli používáte atomické operace tam kde stačilo volatile, potom je to mimořádně špatně.
[42][43] jj já vím, ten komentář jsem napsal proto, že bych nezatracoval jiné interní formáty čísel a ALU operace. A zrovna u céčka, kde není jisté skoro nic (šířka charu například, již zmíněný aritmetický posun u signed hodnota atd. = a právě kvůli dvojkovému doplňku) je defenzivní přístup - resp. respektování normy - docela vhodné. FP16 si dovolím tvrdit, že docela dobře vím co je :-)
Dekuji vsem diskutujicim za podmente pripominky, diky teto diskuzi jsem zjistil, ze gcc-avr podporuje i long long aritmetiku. Jaksi me nenapadlo, ze je to vlastne standard. Dal jsem zjistil, ze tento processor je osmibitovy a ne 32 bitovy - nejak jsem si prilis zvykl na ARM. Pak jsem zjistil, ze gcc-avr preklada naprosto nesmyslnym zpusobem i naprosto jednoduche long long scitani, viz [25]. Ale program fungoval, dokonce lepe nez na PIC, kde jsem to mel v assembleru. Ale nedalo mi to, pustil jsem se do optimalizaci. Nakonec jsem prepsal funkci sum do assembleru a rozepsal cykly ve volajici funkci, aby nerotovala 64 bitove, ale vzala si to byte po byte a rotovala po 8mi bitech. Doba vypoctu sla asi na ctvrtinu.
Pritom jsem jeste zjistil, ze gcc-avr nedokaze odkazovat na registry long long. Odkazy se delaji %A pro nejnizsi byte, %B pro dalsi a tak dale az po %D, ktery odkazuje na nejvyssi registr longu. Ale jaksi zapomneli podporovat %E az %G. Takze temporary registry jsem musel udelat dva longove misto jednoho long long.
No a ted ta funkce na scitani, secte dve unsigned long long cisla, a v navratove hodnote vrati carry:
typedef unsigned long long ull;
unsigned char sum(ull *r, const ull *a) // *r+=*a, returns carry
{
unsigned long int r1;
unsigned long int r2;
asm volatile(
"movw %[z],%[r]" LF
"movw %[y],%[a]" LF
"ld %A[tmp1],Z" LF
"ldd %B[tmp1],Z+1" LF
"ldd %C[tmp1],Z+2" LF
"ldd %D[tmp1],Z+3" LF
"ldd %A[tmp2],Z+4" LF
"ldd %B[tmp2],Z+5" LF
"ldd %C[tmp2],Z+6" LF
"ldd %D[tmp2],Z+7" LF
"ld __tmp_reg__, Y" LF
"add %A[tmp1], __tmp_reg__" LF
"ldd __tmp_reg__, Y+1" LF
"adc %B[tmp1], __tmp_reg__" LF
"ldd __tmp_reg__, Y+2" LF
"adc %C[tmp1], __tmp_reg__" LF
"ldd __tmp_reg__, Y+3" LF
"adc %D[tmp1], __tmp_reg__" LF
"ldd __tmp_reg__, Y+4" LF
"adc %A[tmp2], __tmp_reg__" LF
"ldd __tmp_reg__, Y+5" LF
"adc %B[tmp2], __tmp_reg__" LF
"ldd __tmp_reg__, Y+6" LF
"adc %C[tmp2], __tmp_reg__" LF
"ldd __tmp_reg__, Y+7" LF
"adc %D[tmp2], __tmp_reg__" LF
"st Z, %A[tmp1]" LF
"std Z+1, %B[tmp1]" LF
"std Z+2, %C[tmp1]" LF
"std Z+3, %D[tmp1]" LF
"std Z+4, %A[tmp2]" LF
"std Z+5, %B[tmp2]" LF
"std Z+6, %C[tmp2]" LF
"std Z+7, %D[tmp2]" LF
"clr %A[tmp1]" LF
"rol %A[tmp1]" LF // carry -> tmp1
:[z]"=z"(r), [y]"=y"(a), [tmp1]"=r"(r1), [tmp2]"=r"(r2)
:[r]"r"(r), [a]"r"(a)
);
return r1;
}