Win64 Delphi -koodi voi epäonnistua siellä, missä sama lähdekoodi ajaa siististi Win32:lla, ja HotPDF Delphi PDF -komponentti osui viiteen tällaiseen tapaukseen tuoreella kovetuskierroksella: Power(10, N) sitoutui Single-overloadiin, while-silmukka luki vanhentunutta TList.Countia, High(Int64)-raja pyöristyi arvoon 2^63, 15-numeroinen float-teksti FPC:llä ja testiassertit lakkasivat koostumasta
Mikään näistä ei näy, jos rakennat ja testaat vain Win32:lla, ja juuri niin ne hiipivät sisään. Alla olevat tapaukset tulevat HotPDF:n SVG- ja XPS-tuoista, sen sivurenderöijästä ja JSON-töiden lukijasta, ja siteeratut numeeriset tulokset toistettiin pienillä koeprogrammeilla, jotka koostettiin Win32:lle ja Win64:lle. Jos siirrät Delphi-koodikantaa 64-bittiseen, jokainen on syytä grepata
Miksi Power(10, 100) ylivuotaa vain Win64:llä?
Win64:llä System.Math.Power(10, N) kokonaislukuargumenteilla ratkaisee Single-overloadiin, joten tulos lasketaan ja palautetaan single-tarkkuudella, ja mikä tahansa noin 3.4E38:n yläpuolella ylivuotaa. Win32:lla sama kutsu sitoutuu Extended-overloadiin ja ajaa x87-FPU:lla 80-bittisellä tarkkuudella, joten Power(10, 100) on yksinkertaisesti 1E100
System.Math ilmoittaa funktion Power tyypeille Extended, Double ja Single sekä vastaavan IntPower-perheen, jota Power kutsuu, kun eksponentti on kokonaisluku. Win64:llä Extended on vain alias tyypille Double (SizeOf(Extended) = 8), ja kahdelle kokonaislukuargumentille kääntäjä poimii Single-version. Paljastaja on tarkkuus, ei pelkkä ylivuoto: Win64:llä Power(10, 20) palauttaa arvon 1.0000000200408773E20, joka on täsmälleen Single(1E20). Double-tulos tulostuisi muodossa 1E20. Näimme saman sidonnan jokaisella kokeilemallamme Win64-kääntäjällä, Delphi 10.3:sta kääntäjän versioon 37.0
Sitten tapahtuva riippuu liukulukupoikkeusten maskista. Delphi 12 ja uudemmat maskaavat kaikki liukulukupoikkeukset oletuksena, joten ylivuoto on hiljainen: Power(10, 100) palauttaa arvon +Inf ja Power(10, -100) palauttaa arvon 0. Delphi 11 ja vanhemmat jättävät exOverflowin maskaamatta, ja sama kutsu nostaa EOverflowin. Itse maskin asettavat sovellukset ja tällaisiin isäntiin ladatut DLL:t saavat sen käyttäytymisen, jonka isäntä valitsi, minkä vuoksi kirjasto ei voi olettaa kumpaakaan lopputulosta
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 tulostaa 1E20; Win64 tulostaa 1.0000000200408773E20 (Single-overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Toista se, mitä Delphi 11 tai tiukat FP-asetukset omaava isäntä tekee
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Arvojen exOverflow ja exInvalidOp maskin poistaminen testin ajaksi on halvin tapa nähdä, mitä vanhempi kääntäjä tai tiukka isäntä näkee. Modernilla kääntäjällä oletusasetuksilla bugi ei kaada, se tuottaa äärettömiä ja nollia, ja kyseiset on paljon vaikeampi havaita testilokista. Palauta edellinen maski finallyssa: maski on säike kohtaista tilaa, ja testiajon loput perii sen, mitä jätät taakse
Miten overload yletti HotPDF:n SVG- ja XPS-tuontiin
HotPDF:n SVG- ja XPS-polkulukijat jakavat yhden numeroiden skannerin, ja kyseinen skanneri skaalasi mantissan funktiolla Power(10, Exponent) heti, kun se oli lukenut eksponentin. Mikä tahansa funktiolle THotPDF.ImportSVGFormXObject annettava SVG (se sisääntulopiste, jonka takana on artikkeli SVG:n tuonti PDF:ään uudelleenkäytettävinä form XObjecteina), ja mikä tahansa artikkelin XPS- ja OpenXPS-muunnos PDF:ksi aikana käsitelty polkugeometria, saattoi siis syöttää kyseiseen kutsuun koordinaatin kuten 1e100 tai 5e99
v2.770.91 oli jo rajoittanut eksponentin arvoon 100 ja hylännyt arvot, jotka olisivat päässeet arvon 1E300 ohi, mikä näytti riittävältä: 1E100 on kaukana tyypin Double rajasta noin 1.8E308. Win64:llä se ylivuotti silti, koska laskentaa ei koskaan tapahtunut tyypissä Double lainkaan. Versiosta v2.770.155 alkaen skanneri rakentaa kymmenen potenssin itse, ja luvut kuten 1e-100 tai pitkä mantissa suurella negatiivisella eksponentilla luetaan todelliseen arvoonsa arvoon 0 luhistumisen sijaan
Turvallinen kymmenen potenssi rajatuille eksponenteille
Kun eksponentti on rajattu, turvallisin kymmenen potenssi on se, jonka rakennat itse Double-kertolaskuilla. Enintään 100 kertolaskun silmukka ei maksa mitään sitä ympäröivän tekstin skannaukseen nähden, se ei koskaan tuota väliaikaista, joka olisi suurempi kuin lopullinen skaala, ja se käyttäytyy identtisesti Win32:lla, Win64:llä ja Free Pascalilla
const
MaxDecimalExponent = 100;
function TryScaleByPowerOf10(const Value: Double; Exponent: Integer;
out Scaled: Double): Boolean;
var
Scale: Double;
I: Integer;
begin
Scaled := 0;
Result := False;
if (Exponent < -MaxDecimalExponent) or (Exponent > MaxDecimalExponent) then
Exit;
// Hylkää tulokset, jotka poistuisivat Double-alueelta
if (Exponent > 0) and (Value <> 0) and
(Log10(Abs(Value)) + Exponent > 300) then
Exit;
Scale := 1.0;
for I := 1 to Abs(Exponent) do
Scale := Scale * 10.0; // ei koskaan ylitä arvoa 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // jaa: 1E-100:lla ei ole tarkkaa Doublea
Result := True;
end;
Kolme yksityiskohtaa kantaa painon. Aluetarkistus käyttää kahta vertailua lausekkeen Abs(Exponent) <= 100 sijaan, koska Abs(Low(Integer)) on yhä negatiivinen ja purjehtisi suoraan läpi. Negatiiviset eksponentit jakavat skaalalla etukäteen lasketun arvon 1E-100 kertomisen sijaan, jolla ei ole tarkkaa Doubleia ja joka lisäisi yhden pyöristysvaiheen lisää. Ja Log10-esitarkistus hylkää Double-alueen ulkopuoliset tulokset ennen kuin kertolaskulla on mahdollisuus ylivuota
Ole selvillä siitä, mihin silmukka luopuu. Kymmenen potenssit arvoon 1E22 asti ovat tarkkoja tyypissä Double; sen jälkeen jokainen kertolasku pyöristää, ja 100:n jälkeen skaala istuu muutaman yksikön päälle viimeisessä paikassa oikein pyöristetystä arvosta 1E100. Piirustuskoordinaateille kyseinen on näkymätöntä. Yleiskäyttöiselle teksti–double-muunnokselle, jonka on toistettava jokainen arvo bitti bitiltä, kyseinen ei riitä, ja tarvitset oikein pyöristetyn muunnosalgoritmin sijaan
Milloin dcc64 lukee vanhentunutta TList.Countia while-silmukassa
Havasimme Win64-kääntäjän (dcc64, kääntäjän versio 37.0) tuottavan koodia silmukalle while List.Count > Start do, joka poisti luettelon lopusta ja vertaili pinon väliaikaa vastaan Countin uudelleenlukemisen sijaan. Korjaus, joka sen selvitti, oli silmukka for ... downto, jonka rajat arvioidaan määritelmän mukaan täsmälleen kerran
Silmukka saapui versiossa v2.769.3, joka opetti renderöijän transparenssiryhmäkoodin pitämään ryhmän sisällä luodut pehmeät maskit elossa kaksivaiheisen renderöinnin yli ja vapauttamaan ne sen jälkeen. Siivous istui finally-lohkossa yhden tai kahden vaiheen for-silmukan jälkeen, tiilikohtaisen silmukan sisällä. Muotoonsa pelkistettynä ennen ja jälkeen näyttävät tältä:
// Muoto, jonka näimme virheellisesti koostetun dcc64:llä (kääntäjän versio 37.0)
procedure DropMasksWhile(Masks: TList; Start: Integer);
begin
while Masks.Count > Start do
begin
TObject(Masks[Masks.Count - 1]).Free;
Masks.Delete(Masks.Count - 1);
end;
end;
// Korvaus: rajat arvioidaan kerran, ei vanhentuvaa välimuuttujaa
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count on NativeInt Delphi 12:sta alkaen
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
Generoidussa Win64-koodissa silmukan ehtolon Count ja rungon sisällä luettava Count jakoivat yhden pinopaikan. Ehto vertaili kyseiseen pinopaikkaan tullessaan, ennen kuin mikään oli kirjoittanut siihen, eikä mikään virkistänyt sitä funktion Delete jälkeen. Kun ryhmä ei ollut luonut omia pehmeitä maskejaan, runko ajautui silti käyntiin ja kysyi tyhjältä luettelolta alkiota -1, joten 64-bittisissä koosteissa jokainen tällaisen transparenssiryhmän sisältävä sivu epäonnistui arvolla EListError. Saman lähdekoodin Win32-koodi oli oikein, ja v2.770.1 korvasi silmukan
Emme ole pelkistäneet tätä minimaaliseksi toistoksi, ja pieni itsenäinen silmukka kuten DropMasksWhile saattaa hyvinkin koostua oikein; ympäröivä try/finally ja sisäkkäiset silmukat näyttävät merkitsevän. Käsittele sitä yhdellä kääntäjän versiolla havaittuna koodigeneraationa, ei jokaisen Win64-kääntäjän tunnettuna viana. Käytännön oppi on halvempi kuin juurisyy: silmukka, jonka ehto lukee kokoelman määrän uudelleen samalla kun runko kutistaa kyseistä kokoelmaa, on syytä kirjoittaa uudelleen kiinteäraajaiseksi for ... downto-silmukaksi, ja renderöijämuutokset tarvitsevat täyden Win64-testiajon, ei vain Win32:n
Kaatumisen paikantaminen, jonka vain optimoitu Win64-kooste näyttää
Epäonnistuminen toistui vain optimoidussa Win64-koosteessa, joten sijainti tuli IDE:n ulkopuolisista työkaluista. Pieni koeprogrammi rekisteröi vectored exception handlerin funktiolla AddVectoredExceptionHandler, kaappasi pinon ensimmäisessä poikkeuksessa funktiolla RtlCaptureStackBackTrace ja käänsi paluuosoitteet funktioiden nimiksi käyttäen yksityiskohtaista map-tiedostoa, jonka linkitys kirjoittaa lipulla -GD. Kyseisen funktion disassembly näytti sitten vertailun lukevan pinopaikkaa [rbp+0x298], johon kirjoitettiin vain silmukan rungon sisällä. Kyseinen on todistusaineiston taso, jonka haluat ennen kuin syytät kääntäjää, ja se vei vähemmän aikaa kuin release-koosteen läpi astuminen
Miksi High(Int64) ei ole turvallinen yläraja Doublelle?
Double ei voi esittää arvoa High(Int64): luvun 9223372036854775807 muuntaminen Doubleksi pyöristyy täsmälleen arvoon 2^63, yhden suurinta Int64ä pidemmälle. Win64:llä kyseinen muunnos tapahtuu vertailun itsensä sisällä, joten D <= High(Int64) on True arvolla D = 2^63, ja sitä seuraava Round tai Trunc ylivuottaa
Win32 kätkee tämän samasta syystä, jolla se kätki Power-ongelman. Vertailu ajaa 80-bittisessä Extended-tarkkuudessa 64-bittisellä mantissalla, jossa High(Int64) on tarkka ja 2^63 vertautuu oikein suuremmaksi. Win64:llä ei ole leveämpää tyyppiä, johon palata. Alueen ulkopuolinen muunnos ei ole kaunista muutakaan: Win64-testeissämme Round(2^63) palautti arvon Low(Int64), hiljaisen etumerkin vaihdoksen, riippumatta siitä, oliko exInvalidOp maskattu vai ei. Win32 palauttaa saman arvon maskattuna ja nostaa EInvalidOpin maskaamattomana
| Lauseke | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), poikkeukset maskattu (Delphi 12+ -oletus) | 1E100 | +Inf |
Power(10, 100), exOverflow maskaamaton | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp maskaamaton | EInvalidOp | Low(Int64) |
HotPDF kohtasi tämän asiakirjatöiden arvojen takana olevassa JSON-lukijassa. JSON asettaa ei minkään aluerajaa numeroille, ja vanha serialisoija muutti minkä tahansa arvon, jolla Frac(Value) = 0, kokonaisluvuksi funktiolla Round, joten täysin laillinen arvo 1e19 muuttui joko vääräksi kokonaisluvuksi tai poikkeukseksi maskista riippuen. Versiosta v2.770.169 alkaen kokonaisluku kirjoitetaan kokonaislukuna vain, kun se mahtuu tyyppiin Int64, kaikki muu säilyttää liukulukutekstinsä, ja kokonaislukuhakijat palauttavat kutsujan oletuksen alueen ulkopuolisille arvoille käärityn arvon sijaan
const
TwoPow63 = 9223372036854775808.0; // 2^63, tarkka Doublessa ja Extendedissa
function TryDoubleToInt64(const Value: Double; out R: Int64): Boolean;
begin
R := 0;
Result := not IsNan(Value) and not IsInfinite(Value) and
(Frac(Value) = 0) and (Value >= -TwoPow63) and (Value < TwoPow63);
if Result then
R := Trunc(Value);
end;
function JsonNumberText(const Value: Double): string;
var
R: Int64;
begin
// Kutsujat hylkäävät NaNin ja äärettömät ensin: JSONilla ei ole niille kirjoitusasua
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral pysähtyy 15 numeroon
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Yläraja on literaali 9223372036854775808.0 tiukalla <:llä. Kyseinen vakio on 2^63, tarkka sekä tyypissä Double että Extended, joten vertailu tarkoittaa samaa asiaa jokaisella alustalla. Alaraja voi käyttää merkkiä >=, koska arvo -2^63 on täsmälleen Low(Int64). Funktioiden IsNan ja IsInfinite testaaminen ensin oikosulkuarvioinnilla pitää NaNin ja äärettömät loitolla funktiosta Frac ja vertailuista, jotka voivat nostaa EInvalidOpin, kun isäntä on poistanut sen maskin
Kuinka monta numeroa float–teksti-muunnos oikeastaan antaa Win64:llä?
Vähemmän kuin pyydät, kahdella kolmesta kääntäjästä. Free Pascal 3.3.1:n FloatToStrF(Value, ffGeneral, 17, 0) Win64:llä pysähtyy 15 merkitsevään numeroon, joten 1/3 palautuu muodossa 0.333333333333333, ja kaksi erilaista Double-arvoa voi serialisoitua identtiseksi tekstiksi. Funktio Str(Value:24, Text) jota seuraa Trim tuottaa 17 merkitsevää numeroa tieteellisessä merkinnässä, 3.3333333333333331E-001 samalle arvolle, ja kirjoittaa aina pisteen desimaalierottimeksi lokaalista riippumatta. Jos HotPDF FPC:llä on osa koostumatriisiasi, artikkeli HotPDF Free Pascal- ja Lazarus Win64 -tukimuistiinpanot kattaa alustaerojen loput
Delphi hyväksyy 17 numeron pyynnön, mutta kaksi Delphi-kohdetta eriävät yhä tulosteessa: FloatToStrF(0.1, ffGeneral, 17, 0) antaa arvon 0.10000000000000001 Win32:lla ja arvon 0.1 Win64:llä. Win64-RTL voi myös tuoda viimeisen numeron pyöristysvirheen sekä muotoillessa että jäsentäessä, joten useampi numero kaventaa kuilua takaamatta, että jokainen Double-bittikuvio selviää tekstikierron läpi. HotPDF:n dokumentaatio ei tee kyseistä lupausta, eikä sinunkaan pitäisi tehdä sitä, ellet toimita itse oikein pyöristettyä muotoilijaa ja jäsentäjää. Anna parametri TFormatSettings.Invariant tai korvaa erotin itse vanhemmilla Delphi-versioilla, jottei saksan tai ranskan lokaali kirjoita pilkkua JSONiin
Miksi Assert.AreEqual lakkaa koostumasta Win64:llä?
Assert.AreEqual(3, Length(Arr)) dynaamiselle taulukolle koostuu Win32:lle ja epäonnistuu Win64:llä virheellä E2532, "Couldn't infer generic type argument from different argument types", koska dynaamisen taulukon Length palauttaa arvon NativeInt Win64:llä. Kun toisella puolella on Integer-literaali ja toisella 64-bittinen NativeInt, DUnitX:n geneerinen Assert.AreEqual<T> ei saa selville yksittäistä tyyppiä T, ja kooste pysähtyy
TList.Count laukaisee saman virheen Delphi 12:sta alkaen, jossa ominaisuudesta tuli NativeInt; Delphi 11 ilmoittaa sen yhä muodossa Integer. stringin Length palauttaa arvon Integer molemmilla alustoilla eikä siihen vaikuta, minkä vuoksi virhe ilmestyy joihinkin testiyksiköihin eikä muihin. Kirjoita tyyppiargumentti eksplisiittisesti, Assert.AreEqual<NativeInt>(3, Length(Arr)), ja koosta testiprojekti funktiolla dcc64 ennen sitomista. Sarja, joka rakentaa vain Win32:lle, ei kerro, että sen Win64-kooste on rikki, ennen kuin joku muu kokeilee
Win64-siirtotarkistuslista Delphi-numeeriselle koodille
- Etsi funktioiden
Power(jaIntPower(kutsut kokonaislukuargumenteilla; annaDouble-tyypitetyt arvot tai rakenna rajatut kymmenen potenssit itse - Aja numeeriset testit vähintään kerran poistamalla arvot
exOverflowjaexInvalidOpfunktiollaSetExceptionMask, sekä Win32:lla että Win64:llä - Kirjoita
Int64-yläraja muodossa< 9223372036854775808.0, ei koskaan<= High(Int64), ja hylkää NaN ja äärettömät ennen mitään vertailua - Älä muunna jäsennettyä numeroa arvoon
Int64pelkästään siksi, ettäFracon 0; JSON-numerot voivat olla paljon suurempia - Kirjoita uudelleen
while-silmukat, jotka lukevat arvonCountuudelleen poistaessaan alkioita, kiinteäraajaisiksifor ... downto-silmukoiksi - FPC Win64:llä käytä funktiota
Str(Value:24, Text), kun tarvitset yli 15 merkitsevää numeroa - Käytä muotoa
Assert.AreEqual<NativeInt>arvojenLengthjaCountasserteille ja koosta testit funktiolla dcc64 ennen sitomista - Muutoksen jälkeen mihin tahansa jäsentimeen tai renderöijään aja täysi regressiosarja Win32:lla ja Win64:llä, ei vain toisella
Tässä kuvatut kirjastopuolen korjaukset ovat kaikki HotPDF:ssä versiosta v2.770.169 alkaen, joten SVG-tuonti, XPS-muunnos, transparenssin renderöinti ja JSON-töiden käsittely käyttäytyvät nyt samalla tavalla Win64:llä kuin Win32:lla. Jos generoit tai käsittelet PDF-tiedostoja Delphistä tai C++Builderista molemmille alustoille, sivulta HotPDF Delphi PDF component löytyvät lataukset ja täysi ominaisuusluettelo