C operátor hogyan működik?

Fórumok

Sziasztok!

Adott a következő C programrészlet:

uint16_t a=0x0100;
uint16_t b=0x0100;
uint32_t c;
c=a*b;

Mennyi lesz a c változó értéke?

Szerk.: 16 és 32 bites rendszereken különböző a végeredmény. A specifikáció megfelelő részére keresek linket.

Hozzászólások

Nem vizsga, hanem egy programban találtam eltérő viselkedést PC-n és 8 bites mikrokontrolleren.

Sehol nem találtam meg az idevágó specifikációt, ami megadja, hogy egy műveletet milyen pontossággal kell elvégeznie a programnak, és abban biztam, hogy valaki beidézi az idevágó passzust :-).

Hát a 8 bites mikrovezérlő és C fordítóiról megban a véleményem. Hirdeti magáról a fordító, hogy ANSI C kompatibilis, ennek ellenére már az első órában sikerült írnom egy ANSI C kifejezést, amit nem tudott lefordítani.

Egyszóval több 8 bites mikrovezérlő fordító nem teljesíti maradéktalanul még a legalapabbnak számító ANSI C szabványt sem, bár azt állítja a gyártója, hogy ANSI C-t tudja.

A kovetkezo tortenik amennyiben int szelessege kisebb mint 16 bit.

1. a es b int -re promotalodik.
2. a gep elvegzi a szorzast int ertekekkel
3. c erteket kap azaz az int eredmeny implicit typecast alltal 32 bitesre konvertalodik.

Azaz a 8 bite procinal fuggetlenul attol, hogy az int 16 vagy 8 bites, 16 biten vegzi el a muveletet. Ergo c erteke vaskos 0 -a lesz.

helyesen:
uint32_t c=(uint32_t)a * (uint32_t)b;

Ilyenkor 32 biten szamol.

Akkor talán keressük meg a helyes választ.

Először is, az

uint16_t

és az

uint32_t

opcionálisan típusok, egy C99 implementációnak nem kötelező támogatnia ezeket. (ISO C99 7.18.1.1 Exact-width integer types.) Ha azonban egy implementáción léteznek ezek a típusok, akkor kötelező nekije ezeken a

typedef

-eken is elérhetővé tennie.

Ezután vesézzük ki a szorzást (ISO C99 6.5.5 Multiplicative operators). Először is, a két operanduson végrehajtjuk a usual arithmetic conversion-öket, amelyeknek a legelső lépése jelen esetben a default integer promotions mindkét operanduson. (Lásd 6.3.1.8 Usual arithmetic conversions, valamint 6.3.1.1 Boolean, characters, and integers, 2. bekezdés.)

Ha az

int

olyan, hogy tudja ábrázolni a teljes

uint16_t

értékkészletet, akkor mind

a

, mind

b
int

-re konvertálódik (mindegyik ugyanonnan indul,

uint16_t

-ről). Egyébként pedig mindkettő

unsigned int

-re. Az

unsigned int

típus minimális maximuma 65535 (ISO C99 5.2.4.2.1 Sizes of integer types

<limits.h>

), tehát az feltétlenül tudja ábrázolni az

uint16_t

teljes értékkészletét; ha azonban az

int

-is képes erre (ami platformfüggő), akkor

int

-re megyünk.

Az

uint16_t

konverziós rangja megegyezhet az

int

-ével, megegyezhet a

short

-éval, megegyezhet a

char

-éval, illetve takarhat egy extended unsigned integer típust valahol az

int

rangja alatt, azonban mind a négy esetben (az előző bekezdés szerint)

int

-re vagy

unsigned int

-re "lép elő" mind

a

, mind

b

. Az

uint16_t

nem lehet magasabb rangú az

unsigned int

-nél, mert ahhoz vagy

long

rangúnak kellene lennie (amit az értékkészlete nem tesz lehetővé), vagy az

unsigned int

-nél szélesebb extended típusnak kellene lennie, amit ismét a tartománya nem tesz lehetővé.

Mivel így két azonos típusú operandusunk van (vagy

int

mindkettő, vagy

unsigned int

-- jelöljük ezt a promotált típust most

T

-vel), azért a usual arithmetic conversion-ök mást nem írnak most elő, végrehajtjuk a szorzást.

Ha a szorzás eredménye

T

típusban ábrázolható (figyelem! itt még nincs szó konverzióról, csak arról, hogy maga az eredmény előállítható-e a

T

típusban!), akkor minden rendben van.

Ha nem, akkor már számít, hogy

T

(= a promóció eredménye)

signed int

vagy

unsigned int

lett-e. Ha az utóbbi, akkor semmi gond; a matematikai szorzatot modulo

(UINT_MAX+1)

redukáljuk. Ha

T
signed int

volt, akkor sajnos a túlcsordulás nem definiált, vége a dalnak, innentől bármi megengedett az implementáció számára. (ISO C99 6.2.5 Types, 9. bekezdés, illetve 6.5 Expressions, 5. bekezdés.)

Tegyük fel, hogy megvan az eredmény (

T

típusban, ami

int

vagy

unsigned int

a fentiek szerint). Ezt az értéket az értékadás során konvertáljuk

uint32_t

-re. (ISO C99 6.5.16.1 Simple assignment, 2. bekezdés) Mivel az

uint32_t

előjel nélküli típus, azért a túlcsordulás pontosan definiált.

(Figyelem! Ez a modulo

(UINT_MAX+1)

redukció nem ugyanaz, mint a fenti! A vonatkozó rész itt a 6.3.1.3 Signed and unsigned integers, 2. bekezdés. A fentebbi esetben egy szorzás (matematikai művelet) kiértékelése során az a kérdés kerül elő, hogy az eredmény egyáltalán előállítható-e az adott típusban. Túlcsordulás esetén előjel nélküli típusnál redukció, előjelesnél definiálatlan a viselkedés. Itt viszont egy meglévő értéket konvertálunk másik típusra. Túlcsordulás esetén előjel nélküli céltípusnál ugyanolyan a redukció, mint előbb, azonban előjelesnél sem definiálatlan a viselkedés, hanem vagy implementáció által definiált (= kötelezően dokumentálandó) a numerikus végeredmény, vagy egy impl.def. (= kötelezően dokumentálandó) szignál generálódik.)

Két gyakori kiosztást megnézve (padding biteket mindenhol kizárom, és kettes komplemenst tételezek fel a negatív ábrázoláshoz):

  • 8 bites
    char

    , 16 bites

    short

    , 32 bites

    int

    : a promóció

    signed int

    -re történik. Ha a szorzat nem ábrázolható, akkor undefined behavior. Ez a legtöbb platformon azt eredményezi, hogy a kilógó biteket levágjuk, ami marad, az pedig kettes komplemens ábrázolás szerint lehet negatív is (a sign bit függvényében). Ez történik pl. x86(_64) GNU/Linux-on. A topiknyitóban megadott értékekkel természetesen nincs túlcsordulás, az eredmény 0x10000.

  • 8 bites
    char

    , akárhány bites

    short

    , 16 bites

    int

    : az előléptetés nem látszik szükségesnek (az

    uint16_t

    az

    unsigned int
    typedef

    -je), de ha a platform beteg volna, és valamilyen extended típust használna

    uint16_t

    -re, akkor a promóció

    unsigned int

    -re történik. (6.3.1.1 Boolean, characters, and integers, 1. bekezdés.) A szorzás definiált, eredménye nulla.

  • 16-nál kevesebb bit az
    int

    -ben. Ez nem C99-konform platform.

Aki ezt nem tudja fejből, vagy nem tudja megnézni a szabványban (vagy egy ahhoz nagyon közeli draft-ban), az nagyon óvatosan írjon C programot, mert tele lesz undefined (de minimum implementation defined) behavior-rel. Vagy legalábbis szigorúan ragaszkodjon egy adott platformhoz (hardvert és fordítót beleértve).

A produktivitás miatt. Meg nem lehet minden mikrovezérlő assemblyjét fejben tartani.

De milyen már az, hogy valaki össze akar szorozni két számot, az ábrázolási tartományokba a tényezők is beférnek, és a szorzat is belefér, de az eredmény mégsem lesz megfelelő: én elvárom a XXI. században, hogy egy szorzás eredménye vagy rendesen álljon elő, vagy OverFlowException keletkezzen.

Fuszenecker_Róbert

Nem biztos, hogy baj az; feladathoz eszközt :)

Egyébként:

De milyen már az, hogy valaki össze akar szorozni két számot, az ábrázolási tartományokba a tényezők is beférnek, és a szorzat is belefér, de az eredmény mégsem lesz megfelelő

Hát ez az: a platformtól függően a szorzat nem biztos, hogy belefér. A Java ilyenkor kivételt dob (vagy használsz BigInteger-t). A C viszont azt mondja:

  • Trust the programmer.
  • Don't prevent the programmer from doing what needs to be done.
  • Keep the language small and simple.
  • Provide only one way to do an operation.
  • Make it fast, even if it is not guaranteed to be portable.

A fordító úgy áll hozzá a programodhoz, hogy te tudod, mit csinálsz :)

A Java sima integer aritmetika túlcsordulásra nem dob exception-t, hanem úgy működik, mint C-ben - a tetejét az eredménynek levágja. 0-val osztásra ad csak axception-t. Integer aritmetikára a processzor utasításait használja. Ráadásul elégé hatékony kódot csinál a JIT compilere. Ha integer utasításokat kell ciklusban végrehajtani, arra kb olyan jó, mint a C nyelv maga.

Abból, hogy a C platformfüggően értelmezi a leírt programkódot semmilyen teljesítmény nyereség nem származik, hanem csak rengeteg hiba.

A C-ben pont az a lenyeg, hogy nem gondolkozik helyetted. Igy nem pakolja tele a forditott kodot mindenfele szemettel, es lehet benne hatekonyan mukodo programot irni. (Cserebe ismerni kell rendesen a nyelvet es az adott platformot.) A C-re szeretek ugy gondolni mint egy hordozhato assembly-re, mert kb. az is.

"Valakinek a C# futtatokornyezetet is meg kellett irnia." -- Éljen soká, aki bevállalta :-) Viszont ezzel megspórolt pár ezer mérnökórát a többi programozónak.

"van ahol eleve nem jarhato ut a C#" -- természetesen, a C#/Java/Python/PHP/sh sem csodaszer. De ahol választhatok, ott a legmagasabb absztrakciós szintet fogom választani (feltéve, hogy a futási sebesség és a fogyasztás kevésbé szempont). Mikrovezérlőn én is C-znék vagy C++-oznék. Ha van operációs rendszerem, és azon elmegy a keretrendszerem, akkor azt fogom választani. Miért írjak újra egy kódot, amit előttem valaki már megírt, és ami sokkal fontosabb: letesztelt.

De azt nekem senki se mondja, hogy a C kód fent említett viselkedése normális. Kevés kivételtől eltenintve mindenki azt várná, hogy az eredmény 0x0001_0000.A józan ész ezt doktálja.

Fuszenecker_Róbert

"És szeretem azt, hogy a feladatra koncentrálhatok, nem az eszközre."

Ezt hívják tipikus OOP hozzáállásnak. a nyelv nyalja ki a seggem, amit nem tud megcsinálni ara szükség sincs, problémával akarok foglalkozni nem a nyelvvel szopni...bezzeg amikor Linq-ben nem tudnak valamit megcsinálni, hozzám jönnek sírni. Az nem a nyelvvel szopás? Vagy más thread-ből UI-t basztatni. "Minek az a DispatcherObject? hámérnem oldja meg a háttérben? deszarez!". Jellemző...

"bezzeg amikor Linq-ben nem tudnak valamit megcsinálni" -- A LINQ használata és két szám összeszorzása között van egy kis különbség: az előbbi nem kötelező, az utóbbit viszont elég nehéz mással pótolni.

"Vagy más thread-ből UI-t basztatni." -- csakhogy erről tudunk. De a 256 × 256 = 0 dolog viszont azért került elő, mert a topiknyotó nem tudott mit kezdeni a jelenséggel.
Egy jóérzésű fordító legalább egy warningot dobhatott volna.

Fuszenecker_Róbert

Szerintem meg nincs különbség. Aki az elsőrendű logikát nem érti, az ne akarjon összeadni se, mikroprocesszoron.

Miről tudunk? Odajön/levelet ír/felhív skypeon és megosztja a desktopját/odahív(!!!), hogy ez elszáll. De oda is van írva az exceptionbe, hogy invocation on another thread (vagymi).

És ha én implicit modulo-t akarok a szorzásomhoz? mennyivel hatékonyabb már az (u8)256 * (u8)256 mint a ((u32)256*(u32)256)%256? Na?

"Szerintem meg nincs különbség. Aki az elsőrendű logikát nem érti, az ne akarjon összeadni se, mikroprocesszoron." -- ügyesen lehülyézted az olvasók felét.

"De oda is van írva az exceptionbe, hogy invocation on another thread (vagymi)." -- de legalább oda van írva, hogy hol keresd a hibát. A topiknyitónak még ennyi támpontja sem volt.

"És ha én implicit modulo-t akarok a szorzásomhoz? mennyivel hatékonyabb már az (u8)256 * (u8)256 mint a ((u32)256*(u32)256)%256? Na?" -- lehet csinálni olyan processzort, ahol az utóbbi kifejezés 1 órajel, az első meg 100. Na és? Mit bizonyít ez? Mert a C nyelvi kifejezés és annak futási ideje között semmiféle összefüggés nincsen.

Fuszenecker_Róbert

" lehet csinálni olyan processzort, ahol az utóbbi kifejezés 1 órajel, az első meg 100. Na és? Mit bizonyít ez? Mert a C nyelvi kifejezés és annak futási ideje között semmiféle összefüggés nincsen."

De van, ha tudod, hogy az egyik kifejezesbol gyorsabb kod lesz arra a procira amire dolgozol. Persz, oldjon meg mindent a fordito... :D (es maris fel ora lesz egy par soros program forditasa, a fordito fejlesztesi koltsege meg az Apollo programeval fog vetekedni)

"De van, ha tudod, hogy az egyik kifejezesbol gyorsabb kod lesz arra a procira amire dolgozol." -- akkor máris nem "hardverfüggetlen assembly" :-)

Azért vagyik nyűgös a túlcsordulások nem jelzése miatt (elfogadom azt is, ha bevallja a kütyü, hogy nem tudja megcsinálni), mert egyszer implementáltam egy Viterbi-algoritmust, ami ugye egy hibajavító megoldás. Szépen működött is, ki is javította a hibák nagy részét. Csakhogy (mert a gyanúm utólag bebizonyosodott) időnként túlcsordult a változóm, emiatt pedig nem javított ki olyan hibákat, amelyket 1 napi debugolás és kódhegesztés után már igen. És mivel elvileg sem volt lehetőségem a túlcsordulásokat figyelni (a C kitalálói nem adnak nyelvi elemet a Carry bit figyelésére -- hardverfüggetlen módon legalábbis nem: a kód PC-n készült, de ARM Cortex-M3-on futott végül [akkoriban még az volt a csúcstechnológia]), eléggé trükkös módon kellett megoldanom a problémát. Persze megoldottam, és örült a lelkem.

Mindenesetre a hosszúra sikerült beszélgetésünkből világosan látszik, hogy más-más iskola tanait hirdetjük. Neked hardverközpontúbb a gondolkodásod, nekem meg eredményközpontúbb (az én matematikám szerint a 256×256 = 0 teljesen hibás eredmény). Nem vagyunk egyformák. Még jó, kicsit egysíkú lenne a világ.

Fuszenecker_Róbert

Köszi a bókot. De a burkolt reductio ad hitlerumot észrevettem ám.

A HOL nincs oda irva az uzenetbe, csak az, hogy "másik threadbe akartál belehívni, ezért a szoptálcetli ma a tiéd". a hol az a stack traceben van benne, és ugye release kódban nincs debug info (ugye?), igy nincs sorszámod a stack trace-ben. Ugye. Ja, majd csinálj egy mikrofelmérést, hogy tudják-e az emberek, hogy mik vannak az Exception osztályban a Message-n kívül, meg fogsz lepődni.

Lehetne, de a mai processzorok az előbbit részesítik előnyben :-)

Igen, köztudott, hogy a C az egyetlen nyelv, ahol az aritmetikai műveletek túlcsordulhatnak ;)

Most mellékes, hogy hány nyelvben csordulhatnak még túl, és hogy hogyan.

Az a fontos, hogy a nyelv saját fogalmai és a feladattér ("domain") fogalmai között mekkora a különbség. Ha nincs nyomós ellenérvünk, akkor olyan nyelvet érdemes választani, amely a feladattérhez a lehető legközelebb van (DSL, vagy kényelmes/biztonságos általános nyelv). A C valóban borotvaéles és gyakran igen kevéssé intuitív, ebben a tekintetben azonban csak egy példának szántam.

De azt nekem senki se mondja, hogy a C kód fent említett viselkedése normális. Kevés kivételtől eltenintve mindenki azt várná, hogy az eredmény 0x0001_0000.A józan ész ezt diktálja.

Ezzel mindenesetre értek egyet. A C a saját alkalmazási területén közel tökéletes. Ha az eredmény pontos ábrázolása elsődleges, akkor viszont olyan nyelvet kell választani, ami ezt helyből akarja (nemcsak "tudja"), vagy megfelelő függvény-/osztálykönyvtárat kell használni (C-ben, C++-ban, Java-ban stb).

Én például többnyire kerülöm a C++-t. Nagyon is valós gyakorlati hátrányai vannak, a programozása közben pedig iszonyatos mennyiségű buktatóra kell figyelni. Minden, ami a C-ben macerás, közel ugyanolyan macerás C++-ban is, de bejön még rengeteg új fogalom (többsége automatizmus, ami a "háttérben" zajlik: kivételkezelés, virtuális metódusok, különösen többszörös öröklésnél rejtett mutató-machináció, destruktorok, template-ek), emellett a stílus fontossága minimum egy nagyságrenddel nagyobb. Nincs továbbá semmilyen műszaki szabvány, amely a C++-t mondjuk a UNIX rendszerprogramozással ötvözné, ennek ellenére minden szépreményű C++ programozó azzal kezdi, hogy ír egy C++ IO/threading library-t, kivételekkel, örökléssel, wrapper class-okkal, aztán csak néz, hogy hányféleképpen tud fejreállni.

A C++-nak sokkal nagyobb a kifejezőereje, ezért sokkal nehezebb is rendesen használni. Nagyobb nyelvi odafigyelést igényel, a stílus/tervezés (mint említettem) kritikus -- sokkal inkább "mit ne használjunk", mint "mit használjunk" --, rengeteg plusz fogalmat kell bebiflázni (vagy folyton megnézni) a szabványban. Akik dobálózni szoktak a C++ programozásával, azok többnyire nem ismerik a szabványt.

Sosem. Nem programoztam 1995 és 1997 között assemblyben, nem kezdtem el egy operációs rendszert írni és nem írtam egy (csúnya, de működő) grafikus felületet sem.
Sosem láttam még assemblyt. És C-t sem. C++ meg aztán végképp nem.
Fogalmam sincs, hogy mikor számít a kód hatákonysága, és mikor a programozó produktivitása.

Fuszenecker_Róbert

Ugyanennek (vagyis a túl későn végzett típus-váltásnak) egy változata:

rossz:

int a= 1;
int b= 2;
double d= a/b;

jó:

int a= 1;
int b= 2;
double d= (double)a/b;

A C fordító a fenti példa esetén egyébként a C-s logika szerint rendben dolgozik:

32 bit = 16 bit * 16 bit
ami valójában
32 bit = (32 bitre kasztol az eredményregiszter miatt) (16 bit * 16 bit)
Tehát ez a fenti 0x100-as értékek esetén 0-át hozza eredményül.

Ha bármelyik operandus 32 bites, akkor
32 bit = 32 bit * (32 bitre kasztol)16 bit

Ezt úgy is elérheted, ha
c = (uint32_t)a * b;

Azaz csak az "a" 32 bitesre való átalakítása már 32 bites aritmetikába rántja a "b" operandust is.

És hogy miért így csinálja? Mert így gyors, hiszen csak egyszer kasztolt 32 bitre, az aritmetika viszont 16 bites maradt. Ha a 16 bites aritmetikával való gyors számolásról a programozó látja, hogy nem fog elférni 16 biten, akkor a programozónak kell figyelmeztetnie a C fordítót, hogy 32 bites aritmetikával dolgozzon végig és ne csak a végén egyszer konvertáljon.

A C ezért nehéz nyelv. Semmi automatizmus, semmi plusz vizsgálat. A programozó amit leír, az hajtódik végre. Ellenben GYORSAN!