Win64 Delphi kodu, aynı kaynağın Win32'de tertemiz koştuğu yerde başarısız olabilir ve HotPDF Delphi PDF bileşeni yakın bir sağlamlaştırma geçişinde beş böyle olguya çarptı: Single overload'una bağlanan Power(10, N), bayat bir TList.Count okuyan while döngüsü, 2^63'e yukarı yuvarlanan bir High(Int64) sınırı, FPC'de 15 haneli float metni ve derlemeyi durduran test assertion'ları
Yalnızca Win32 derleyip sınarsanız hiçbiri görünmez; içeri sızmalarının yolu da tam olarak budur. Aşağıdaki olgular HotPDF'in SVG ile XPS içe aktarıcılarından, page renderer'ından ve JSON iş okuyucusundan gelir; aktarılan sayısal sonuçlar, Win32 ile Win64 için derlenmiş küçük sonda programlarıyla yeniden üretildi. Bir Delphi kod tabanını 64 bite taşıyorsanız her biri bir grep'e değer
Power(10, 100) neden yalnızca Win64'te taşar?
Win64'te integer argümanlı System.Math.Power(10, N), Single overload'una çözülür; sonuç tek hassasiyette hesaplanır, tek hassasiyette döndürülür ve kabaca 3.4E38 üstündeki her şey taşar. Win32'de aynı çağrı Extended overload'una bağlanır ve 80 bit hassasiyetli x87 FPU üzerinde koşar; dolayısıyla Power(10, 100) yalınca 1E100'dür
System.Math, Extended, Double ve Single için Power bildirir; üssü tam sayı olduğunda Power'ın çağırdığı eşleşen bir IntPower ailesiyle birlikte. Win64'te Extended, yalnızca Double için bir takma addır (SizeOf(Extended) = 8) ve iki integer argüman için derleyici Single sürümünü seçer. Ele veren şey yalnızca taşma değil, hassasiyettir: Win64'te Power(10, 20), tam olarak Single(1E20) olan 1.0000000200408773E20 döndürür. Bir Double sonuç 1E20 basardı. Aynı bağlamayı, Delphi 10.3'ten derleyici sürüm 37.0'a dek denediğimiz her Win64 derleyicisinde gördük
Sonradan ne olduğu kayan nokta istisna maskesine bağlıdır. Delphi 12 ve sonrası bütün kayan nokta istisnalarını varsayılan olarak maskeler; dolayısıyla taşma sessizdir: Power(10, 100), +Inf döndürür ve Power(10, -100) 0 döndürür. Delphi 11 ve öncesi exOverflow'u maskesiz bırakır ve aynı çağrı EOverflow fırlatır. Maskeleri kendisi set eden uygulamalar ve böyle hostlara yüklenen DLL'ler, hostun seçtiği davranışı alır; bir kütüphanenin iki sonucu da varsayamaması bundandır
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 1E20 basar; Win64 1.0000000200408773E20 basar (Single overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Delphi 11'in ya da katı FP ayarlı bir hostun yaptığını yeniden üretir
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Bir test boyunca exOverflow ile exInvalidOp'yi maskesiz bırakmak, eski bir derleyicinin ya da katı bir hostun gördüğünü görmenin en ucuz yoludur. Varsayılan ayarlı modern bir derleyicide hata çökmez; sonsuzlar ve sıfırlar üretir ve onlar bir test logunda çok daha zor fark edilir. Önceki maskeyi finally'de geri koyun: maske thread-başı bir durumdur ve test koşusunun gerisi ardınızda bıraktığınızı miras alır
Overload HotPDF SVG ve XPS içe aktarımına nasıl girdi?
HotPDF'in SVG ile XPS path okuyucuları tek bir sayı tarayıcısını paylaşır ve o tarayıcı bir üs okuduktan sonra mantisi Power(10, Exponent) ile ölçekliyordu. THotPDF.ImportSVGFormXObject'a geçen her SVG (SVG'yi yeniden kullanılabilir form XObject olarak PDF'e içe aktarmayazısının ardındaki giriş noktası) ve XPS ile OpenXPS'in PDF'e dönüştürülmesisırasında işlenen her path geometrisi, o çağrıya 1e100 ya da 5e99 gibi bir koordinat besleyebiliyordu
v2.770.91 üssü hâlihazırda 100'de tavanlamış ve 1E300'ü aşacak değerleri reddetmişti; bu yeterli görünüyordu: 1E100, kabaca 1.8E308 olan Double sınırının çok uzağındadır. Win64'te yine de taşıyordu; çünkü hesap hiçbir zaman Double içinde yapılmıyordu. v2.770.155'ten beri tarayıcı onun kuvvetini kendisi kurar ve 1e-100 gibi sayılar, ya da büyük negatif üslü uzun bir mantis, 0'a çökermek yerine gerçek değerleriyle okunur
Sınırlı üsler için güvenli bir onun kuvveti
Üs sınırlıyken en güvenli onun kuvveti, Double çarpmasıyla kendiniz kurduğunuz kuvvettir. En çok 100 çarpmalı bir döngü, çevresindeki metni taramaya kıyasla hiçbir şeye mal olmaz, son ölçekten büyük bir ara değer asla üretmez ve Win32, Win64 ile Free Pascal'da birebir aynı davranır
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;
// Double aralığından çıkacak sonuçları reddeder
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; // asla 1E100'ü aşmaz
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // böl: 1E-100'ün tam bir Double'ı yoktur
Result := True;
end;
Üç ayrıntı ağırlığı taşır. Aralık denetimi, Abs(Exponent) <= 100 yerine iki karşılaştırma kullanır; çünkü Abs(Low(Integer)) hâlâ negatiftir ve düpedüz geçerdi. Negatif üsler, önceden hesaplanmış 1E-100 ile çarpmak yerine ölçeğe böler; o değerin tam bir Double karşılığı yoktur ve bir yuvarlama adımı daha eklerdi. Ve Log10 ön denetimi, çarpmanın taşma şansı kalmadan Double aralığı dışındaki sonuçları reddeder
Döngünün neye feragat ettiğini açıkça söyleyin. 1E22'ye kadarki onun kuvvetleri Double'da tamdır; ötesinde her çarpma yuvarlar ve 100 çarmadan sonra ölçek, doğru yuvarlanmış 1E100'ün son yerden birkaç unit uzağında durur. Çizim koordinatları için bu görünmezdir. Her değeri bit bit yeniden üretmek zorunda olan genel amaçlı bir metin-çevirme-Double dönüşümü için yeterli değildir; doğru yuvarlayan bir dönüşüm algoritmasına ihtiyacınız olur
dcc64 bir while döngüsünde bayat TList.Count'ı ne zaman okur?
Win64 derleyicisini (dcc64, derleyici sürüm 37.0), listenin sonundan silen ve Count'u yeniden okumak yerine bir stack geçicisiyle karşılaştıran bir while List.Count > Start do döngüsü için kod üretirken gördük. Onu düzelten yeniden yazım, sınırları tanım gereği tam olarak bir kez değerlendirilen bir for ... downto döngüsüydü
Döngü v2.769.3'te gelmişti; o sürüm, renderer'ın transparency-group koduna bir grup içinde kurulan soft maskeleri iki geçişli bir render boyunca canlı tutmayı ve ardından free etmeyi öğretmişti. Temizlik, tek ya da iki geçişli bir for döngüsünün ardından gelen bir finally bloğunda, kiremit-başı döngünün içindeydi. Biçimine indirgenmiş hâliyle önce ve sonra şöyle görünür:
// dcc64'ün (derleyici sürüm 37.0) yanlış derlediğini gördüğümüz biçim
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;
// Değiştirme: sınırlar bir kez değerlendirilir, bayatacak geçici yok
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count, Delphi 12'den beri NativeInt
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
Üretilen Win64 kodunda döngü koşulundaki Count ile gövde içinde okunan Count tek bir stack yuvasını paylaşıyordu. Koşul girişte, henüz hiçbir şey onu yazmamışken o yuva ile karşılaştırıyordu ve Delete sonrasında onu tazeleyen hiçbir şey yoktu. Bir grup kendi soft maskesini hiç kurmamışsa gövde yine de koşuyor ve boş bir listeden -1 numaralı girdiyi istiyordu; böylece 64 bitlik derlemelerde böyle bir transparency grubu taşıyan her sayfa EListError ile düşüyordu. Aynı kaynağın Win32 kodu doğrudu ve v2.770.1 döngüyü değiştirdi
Bunu minimal bir yeniden üretime indirgemedik ve DropMasksWhile gibi küçük bir bağımsız döngü büyük olasılıkla doğru derlenir; çevredeki try/finally ile iç içe döngüler önemli görünüyor. Bunu her Win64 derleyicisinin bilinen bir kusuru olarak değil, tek bir derleyici sürümünde gözlemlediğimiz kod üretimi olarak ele alın. Pratik ders daha ucuzdur: koşulu gövde küçültürken bir koleksiyonun sayısını yeniden okuyan bir döngüyü sabit sınırlı bir for ... downto olarak yeniden yazmaya değer ve renderer değişikliklerinin salt Win32 değil tam bir Win64 test koşusuna ihtiyacı vardır
Yalnızca optimize edilmiş Win64 derlemesinin gösterdiği çökmeyi bulmak
Başarısızlık yalnızca optimize edilmiş Win64 derlemesinde yeniden üretildi; dolayısıyla konum, IDE dışındaki araçlardan geldi. Küçük bir sonda programı AddVectoredExceptionHandler ile bir vectored exception handler kaydetti, ilk istisyada stack'i RtlCaptureStackBackTrace ile yakaladı ve dönüş adreslerini, linker'ın -GD ile yazdığı ayrıntılı map dosyasını kullanarak fonksiyon adlarına çevirdi. O fonksiyonu disassemble etmek ise karşılaştırmanın, yalnızca döngü gövdesinin içinde yazılan bir stack yuvasını — [rbp+0x298] — okuduğunu gösterdi. Bir derleyiciyi suçlamadan önce isteyeceğiniz kanıt düzeyi budur ve release bir derlemede adım adım ilerlemekten az zaman aldı
High(Int64) bir Double için neden güvenli bir üst sınır değildir?
Bir Double, High(Int64)'i temsil edemez: 9223372036854775807'yi Double'a çevirmek tam olarak 2^63'e, en büyük Int64'ün bir fazlasına yuvarlanır. Win64'te o dönüşüm karşılaştırmanın içinde olur; dolayısıyla D <= High(Int64), D = 2^63 için True'dur ve ardından gelen Round ya da Trunc taşar
Win32 bunu, Power sorununu sakladığı aynı nedenle saklar. Karşılaştırma, 64 bitlik mantisli 80 bit Extended hassasiyetinde koşar; orada High(Int64) tamdır ve 2^63 doğru biçimde büyük karşılaştırılır. Win64'ün yaslanacağı daha geniş bir tip yoktur. Aralık dışı dönüşüm de güzel değildir: Win64 testlerimizde Round(2^63), exInvalidOp maskeli de maskesiz de, sessiz bir işaret çevirmesi olan Low(Int64) döndürdü. Win32 maskeliyken aynı değeri döndürür, maskesizken EInvalidOp fırlatır
| İfade | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), istisnalar maskeli (Delphi 12+ varsayılanı) | 1E100 | +Inf |
Power(10, 100), exOverflow maskesiz | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp maskesiz | EInvalidOp | Low(Int64) |
HotPDF bunla, belge iş değerlerinin ardındaki JSON okuyucusunda karşılaştı. JSON sayılara aralık sınırı koymaz ve eski serileştirici, Frac(Value) = 0 olan her değeri Round ile bir integer'a çeviriyordu; böylece tamamen meşru bir 1e19, maskeye bağlı olarak ya yanlış bir integer'a ya bir istisyona dönüşüyordu. v2.770.169'dan beri tam sayı, yalnızca Int64'e sığdığında integer yazılır, geri kalan her şey kayan nokta metnini korur ve integer getter'lar, aralık dışı değerler için sarmalanmış bir değer yerine çağıranın varsayılanını döndürür
const
TwoPow63 = 9223372036854775808.0; // 2^63, Double'da ve Extended'ta tam
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
// Çağıranlar önce NaN ile sonsuzları reddeder: JSON için yazımı yoktur
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral 15 hanede durur
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Üst sınır, katı bir < ile yazılmış 9223372036854775808.0 literalidir. O sabit 2^63'tür ve Double'da da Extended'ta da tamdır; dolayısıyla karşılaştırma her platformda aynı anlamı taşır. Alt sınır, -2^63 tam olarak Low(Int64) olduğundan >= kullanabilir. IsNan ile IsInfinite sınamalarını kısa devre değerlendirmeyle önce yapmak, NaN ile sonsuzları Frac'ten ve host maskesini kaldırmışsa EInvalidOp fırlatabilecek karşılaştırmalardan uzak tutar
Win64'te float-metin dönüşümü size gerçekten kaç hane verir?
İstediğinizden azını, üç derleyiciden ikisinde. Free Pascal 3.3.1'in Win64'teki FloatToStrF(Value, ffGeneral, 17, 0)'u 15 anlamlı hanede durur; böylece 1/3, 0.333333333333333 olarak döner ve iki farklı Double değeri aynı metne serileşebilir. Str(Value:24, Text) ardından Trim, bilimsel gösterimde 17 anlamlı hane üretir — aynı değer için 3.3333333333333331E-001 — ve yerel ayardan bağımsız olarak ondalık ayracı olarak daima nokta yazar. FPC üzerindeki HotPDF derleme matrisinizin parçasıysa, HotPDF Free Pascal ve Lazarus Win64 destek notları platform farklarının gerisini kapsar
Delphi 17 haneli isteği kabul eder ama iki Delphi hedefi çıktıda hâlâ anlaşamaz: FloatToStrF(0.1, ffGeneral, 17, 0), Win32'de 0.10000000000000001, Win64'te 0.1 verir. Win64 RTL, biçimlendirirken de ayrıştırırken de son hane yuvarlama hatası ekleyebilir; dolayısıyla daha çok hane aralığı daraltır ama her Double bit örüntüsünün metin gidiş-dönüşünden sağ çıktığını garanti etmez. HotPDF'in belgeleri böyle bir vaat vermez; sizinki de kendi doğru yuvarlayan biçimlendiricinizi ve ayrıştırıcınızı göndermedikçe vermemelidir. Alman ya da Fransız bir yerel ayarın JSON'a virgül yazmaması için TFormatSettings.Invariant geçirin ya da ayracı eski Delphi sürümlerinde kendiniz değiştirin
Assert.AreEqual Win64'te neden derlemeyi bırakır?
Dinamik bir dizi üzerinde Assert.AreEqual(3, Length(Arr)) Win32 için derlenir ve Win64 için E2532 ile düşer: "Couldn't infer generic type argument from different argument types". Nedeni, dinamik bir dizinin Length'inin Win64'te NativeInt döndürmesidir. Bir yanda Integer literal, öte yanda 64 bitlik bir NativeInt varken DUnitX'in generic Assert.AreEqual<T>'si tek bir T üzerinde anlaşamaz ve derleme durur
TList.Count, property'nin NativeInt olduğu Delphi 12'den beri aynı hatayı tetikler; Delphi 11 onu hâlâ Integer olarak bildirir. Bir string'in Length'i her iki platformda da Integer döndürür ve etkilenmez; hatanın bazı test unit'lerinde görünip bazılarında görünmemesi bundandır. Tip argümanını açıkça yazın — Assert.AreEqual<NativeInt>(3, Length(Arr)) — ve commit etmeden önce test projesini dcc64 ile derleyin. Yalnızca Win32 için derlenen bir takım, Win64 derlemesinin bozuk olduğunu başkası denemeden söylemez
Delphi sayısal kodu için Win64 taşıma kontrol listesi
- Integer argümanlı
Power(ileIntPower(çağrılarını arayın;Doubletipli değerler geçirin ya da sınırlı onun kuvvetlerini kendiniz kurun - Sayısal testleri,
SetExceptionMasküzerindenexOverflowileexInvalidOp'yi kaldırarak hem Win32'de hem Win64'te en az bir kez koşturun Int64üst sınırını< 9223372036854775808.0olarak yazın, asla<= High(Int64)değil, ve her karşılaştırmadan önce NaN ile sonsuzları reddedin- Ayrıştırılmış bir sayıyı yalnızca
Fracsıfır olduğu içinInt64'e çevirmeyin; JSON sayıları çok daha büyük olabilir - Silme yaparken
Count'u yeniden okuyanwhiledöngülerini sabit sınırlıfor ... downtodöngüleri olarak yeniden yazın - FPC Win64'te 15 anlamlı haneden fazlasına ihtiyaç duyduğunuzda
Str(Value:24, Text)kullanın LengthileCountassertion'ları içinAssert.AreEqual<NativeInt>kullanın ve commit etmeden önce testleri dcc64 ile derleyin- Herhangi bir parser ya da renderer değişikliğinden sonra tam regresyon takımını Win32'de de Win64'te de koşturun, yalnızca birinde değil
Burada anlatılan kütüphane tarafı düzeltmelerin hepsi HotPDF'te v2.770.169'dan beri var; böylece SVG içe aktarma, XPS dönüşümü, transparency rendering ve JSON iş işleme, Win64'te artık Win32'dekiyle aynı davranıyor. Delphi ya da C++Builder'dan her iki platform için de PDF üretiyor ya da işliyorsanız indirmeler ile tam özellik listesi HotPDF Delphi PDF component sayfasındadır