Bir milyon satırlık ve bir düzine (dozen) sütunluk bir elektronik tablo, bir veritabanı raporlama işinden gelen gayet sıradan bir dışa aktarımdır. Tüm çalışma kitabını (workbook) bir TXLSWorkbook içine yükleyerek her zamanki yolla açtığınızda işlem, ilk iş mantığı satırınız çalışmadan önce o on iki milyon hücrenin her birini canlı bir nesne olarak somutlaştırmak (materialise) zorundadır. Diskteki dosya altmış megabaytlık sıkıştırılmış bir XML olabilir. Genişlediği nesne ağacı bunun birkaç katıdır ve hepsinin aynı anda yerleşik (resident) olması gerekir çünkü model tasarımı gereği rastgele erişimlidir (random-access). Yukarıdan aşağıya okumak ve çöpe atmak niyetinde olduğunuz bir rapor için, bu hiç ihtiyaç duymadığınız bir yapıya harcanan çok büyük bir bellektir
Aynı dosyadan geçen ikinci bir yol daha vardır. Bir model inşa etmek yerine, çalışma sayfası XML'ini hücre hücre yalnızca ileriye doğru (forward only) tararsınız ve baktıktan sonra her hücrenin akıp gitmesine izin verirsiniz. Hiçbir şey birikmez. İster bin, ister on milyon satır olsun bellek neredeyse sabit kalır, çünkü okuyucu hiçbir zaman şu anda ayrıştırdığı (parsing) parça ve birkaç küçük arama tablosundan (lookup tables) fazlasını tutmaz. HotXLS doğrudan okuyucusu (direct reader) bunu yapar ve bu makalenin geri kalanı neden küçük kaldığı ve buna karşılık size ne verdiği hakkındadır
Bellek içi model neden ölçeklenmez
Bir XLSX dosyası, ECMA-376 tarafından tanımlanan XML bölümlerinden (parts) oluşan bir ZIP paketidir. Her çalışma sayfası kendi bölümüdür (xl/worksheets/sheetN.xml) ve içinde her satır, <c> hücre öğelerini (elements) tutan bir <row> öğesidir. Düzenli (regular) yükleme yolu o bölümü okur ve her bir hücre için adreslenebilir (addressable) bir nesne oluşturur, böylece daha sonra Cells[12345, 7]'yi isteyebilir ve sabit (constant) zamanda bir yanıt alabilirsiniz. Rastgele erişim (Random access), bir çalışma kitabı modelinin tüm amacıdır ve düzenlemeyi (editing), formül değerlendirmesini (formula evaluation) ve stillendirmeyi (styling) kolay kılan da tam olarak budur
Maliyeti ise rastgele erişimin her şeyin aynı anda mevcut olmasını gerektirmesidir. Sadece kısmen oluşturduğunuz bir yapı içerisine dizin (index) koyamazsınız. Dolayısıyla tam bir yüklemenin (full load) en yüksek belleği (peak memory) hücre sayısının bir işlevidir ve milyonlarca dolu hücreye (populated cells) sahip bir sayfada bu işlev (function), özellikle de paylaşılan bir makinede (shared machine) aynı anda birkaç iş yürütülüyorsa hizmetinizin (service) olmak istemeyeceği bir yere iner. Gerçekte ihtiyaç duyduğunuz erişim düzeni ardışıksa (sequential), rastgele erişim için ödeme yapmak kullanmayacağınız bir yetenek (capability) için ödeme yapmaktır
Ağaç oluşturmayan yalnızca ileri (forward-only) SAX taraması
Doğrudan okuyucu ZIP paketini açar ve SAX tarzı bir çekme ayrıştırıcısı (pull parser) ile her çalışma sayfası bölümünü yürütür (walks). Buradaki SAX, ayrıştırıcının ayrıştırma olaylarını karşılaştıkça (bir başlangıç öğesi, bir metin akışı, bir bitiş öğesi) raporladığı ve ardından devam ettiği anlamına gelir. Arkasında hiçbir düğüm (node) ağacı bırakmaz. Okuyucu geçerli satırı ve sütunu r özniteliklerinden (attributes) izler, olaylar geldiğinde hücrenin türünü, stil dizinini (style index), değerini ve formül metnini toplar ve kapanış </c> etiketi görüldüğünde bir hücre yayar (emits) ve onu unutur. Sonraki hücre aynı bir avuç (handful) yerel değişkeni (variables) yeniden kullanır
Hücreler arasında hiçbir şey tutulmadığından, bellek ayak izi (memory footprint) hücre sayısıyla artmaz (grow). Tutunmaya değer olan mülk (property) budur. İki yüz satırlık bir sayfa ve yirmi milyon satırlık bir sayfa okuyucuya aynı yerleşik belleğe mal olur ve aralarındaki fark yalnızca taramanın ne kadar süreceğidir. Modelin başyazı (headline) özelliği olan rastgele erişimden vazgeçersiniz ve karşılığında hücre sayısının geçemeyeceği bir bellek tavanı (ceiling) alırsınız
Ne yerleşik (resident) kalır ve neden o iki parça
Tarama tamamen durumsuz (stateless) değildir ve istisnalar (exceptions) öğreticidir (instructive). Kendi başına bir hücre, onlar olmadan (without them) yorumlamaya (interpret) yetecek kadar bilgi taşımadığından, süre boyunca bellekte tutulması gereken iki küçük tablo (tables) vardır
Birincisi paylaşılan dize (shared string) tablosudur. SpreadsheetML'de, bir metin hücresi kendi metnini depolamaz. t="s" ve çalışma kitabındaki her farklı (distinct) dizenin (string) tekilleştirilmiş (deduplicated) tek bir listesi olan xl/sharedStrings.xml'in (sharedStrings.xml) içine bir indeks (index) olan sayısal bir yük (payload) taşır. Bu, aynı etiketlerin binlerce satırda tekrarlandığı dosyalar için iyi bir alan (space) takasıdır, ancak okuyucunun bu dize tablosunu en baştan (up front) yüklemesi ve yerleşik tutması gerektiği anlamına gelir çünkü herhangi bir sayfadaki herhangi bir hücre içindeki (in it) herhangi bir girdiye (entry) atıfta (reference) bulunabilir. Tablo, hücre sayısına göre değil farklı dize (distinct strings) sayısına göre boyutlandırılmıştır (sized), bu nedenle devasa sayfalarda (enormous sheets) bile mütevazı (modest) kalır
İkincisi, stiller (styles) bölümünden sayı biçimi (number-format) haritalamasıdır (mapping). Sayısal bir hücre (numeric cell) ve bir tarih hücresi (date cell) tel (wire) üzerinde baytı baytına (byte-for-byte) aynıdır: her ikisi de düz (plain) bir sayıdır, çünkü SpreadsheetML'de bir tarih yalnızca seri bir gün sayımıdır. Onları ayıran tek şey (The only thing that distinguishes them is), xl/styles.xml içindeki cellXfs üzerinden bir sayı biçimi kimliğine (number-format id) işaret eden (points) hücrenin stilidir. Bir tarihi ham seri numarası (raw serial number) yerine bir tarih olarak raporlamak için, okuyucu bu stilden formata (style-to-format) tablosunu yükler ve onu yerleşik (resident) tutar. Dosyadaki diğer her şey, yani baytların çoğunu (bulk of the bytes) oluşturan gerçek hücre verisi (actual cell data), depolanmadan (stored) akıp (streams) geçer
Her hücre bir tür (kind) ve bir değer raporlar
Yayılan (emitted) her hücre bir TXLSDirectCell kaydı (record) olarak gelir. Sayfa dizinini (sheet index) ve adını, 1 tabanlı (1-based) satır ve sütunu, anlamsal (semantic) bir Kind'ı, Variant olarak Value'yu, baştaki eşittir (equals) işareti olmadan Formula metnini ve ham (raw) StyleIndex'i taşır. Tür (kind), xdkNumber, xdkString, xdkBoolean, xdkDate veya xdkError'dan biridir, böylece hücrenin ne anlama geldiğini özniteliklerden (attributes) yeniden türetmek (re-deriving) yerine üzerinde dallanabilirsiniz (branch on). Formül hücresi, formül metni ile birlikte önbelleğe alınmış (cached) sonucunun türünü (kind) raporlar, böylece hesaplanan bir toplam (computed total) aynı zamanda size nasıl üretildiğini (produced) de anlatan bir sayı olarak gelir
type
TReportScan = class
procedure OnCell(Sender: TObject; const Cell: TXLSDirectCell;
var Abort: Boolean);
end;
procedure TReportScan.OnCell(Sender: TObject; const Cell: TXLSDirectCell;
var Abort: Boolean);
begin
case Cell.Kind of
xdkString: AccumulateLabel(Cell.Row, Cell.Col, VarToStr(Cell.Value));
xdkNumber: AddToTotals(Cell.Col, Double(Cell.Value));
xdkDate: NoteWhen(Cell.Row, VarToDateTime(Cell.Value));
xdkBoolean: FlagRow(Cell.Row, Boolean(Cell.Value));
xdkError: LogBadCell(Cell.Row, Cell.Col, VarToStr(Cell.Value));
end;
end;
Bir tarihi sayıdan ayırt etmek
Tarih sorusu daha yakından (closer) bir bakışı (look) hak ediyor çünkü saf tarayıcıların (naive scanners) çoğu (most) burada hata yapıyor. Sayısal bir hücrenin tarih türü (date type) yoktur. 46000 seri değerini (serial value) taşıyan bir hücre bir nicelik (quantity), bir fiyat (price) veya 17 Şubat 2025 olabilir ve dosya size hangisi olduğunu ancak (only through) hücrenin stili (style) aracılığıyla erişilen sayı-biçimi kimliği (number-format id) ile söyler. ECMA-376, anlamı uyan (conforming) her üretici (producer) genelinde sabitlenmiş yerleşik (built-in) biçim kimliklerinden oluşan bir bloğu saklı tutar ve tarih taşıyan (date-bearing) kimlikler iki aralıkta (ranges) bulunur: standart tarih ve saat biçimleri (standard date and time formats) için 14 ila 22; ve [h]:mm:ss gibi geçen zaman (elapsed-time) biçimleri için 45 ila 47. Varsayılan olarak açık olan DetectDates açık olduğunda, okuyucu her sayısal hücrenin stilini biçim kimliğine çözümler ve kimliği bu ayrılmış (reserved) aralıklara giren (falls) bir hücre, Value değeri (Value) halihazırda bir Delphi TDateTime'a dönüştürülmüş olarak xdkDate olarak rapor edilir. Özel biçimler (Custom formats) de, tarih ve saat belirteçleri (tokens) için biçim kodunu inceleyerek (inspecting) denetlenir, ancak ayrılmış aralıklar (reserved ranges) güvenilir (dependable) bir omurgadır (backbone). DetectDates özelliğini kapattığınızda stiller tablosu hiç yüklenmez, her sayısal hücre xdkNumber olarak gelir ve tarama (scan) kısmen (fractionally) daha zayıf (leaner) olur
Sayfaları atla ve erken iptal et
Sıralı tarama (Sequential scanning), rastgele erişimin eşleşemeyeceği sessiz bir avantaja sahiptir: durabilirsiniz. OnSheet olayı her çalışma sayfasının (worksheet) açılmasından önce tetiklenir (fires) ve size iki anahtar (switches) verir. SkipSheet ayarladığınızda bu bölümün (part) tamamı hiçbir zaman ayrıştırılmaz (parsed) ki bu, çok sayfalı (multi-sheet) bir çalışma kitabında geri kalanını okumak için ödeme yapmadan (paying to read) yalnızca ilgilendiğiniz sayfaları tarama şeklinizdir (how you scan). Abort ayarladığınızda tüm tarama hemen (immediately) sona erer (ends). OnCell olayının (event) kendine ait bir Abort'u vardır, bu nedenle milyonlarca kalan (remaining) hücreyi okumadan, aradığınızı -belirli bir satır, nöbetçi bir değer (sentinel value), bir başlık bloğunun (header block) sonu- bulduğunuz an durdurabilirsiniz (halt). Sadece ileri yönlü (forward-only) bir taramada iptal etme (abort) gerçekten (genuinely) ücretsizdir, çünkü atladığınız iş henüz gerçekleşmemiş (had not happened yet) olandır
procedure TReportScan.OnSheet(Sender: TObject; SheetIndex: Integer;
const SheetName: WideString; var SkipSheet: Boolean; var Abort: Boolean);
begin
// Scan only the "Data" sheet; leave the rest unread
SkipSheet := SheetName <> 'Data';
end;
Bir işleyici (handler) olmadan hücreleri sayma
Son zamanlarda yapılan bir iyileştirme (refinement), yaygın bir soruyu ucuz (cheap) bir tek çağrıya dönüştürdüğü (turns) için belirtmeye değer. Okuyucu, geçtiği (passes) her dolu (populated) hücreyi sayar (counts) ve bunu bir OnCell işleyicisi (handler) bağlı (attached) olsun veya olmasın yapar. Daha önce, bir işleyici ayarlanmamışken dolu hücre sayısı, sayım (counting) yaymanın (emitting) bir yan etkisi (side effect) olduğu için sıfır (zero) olarak geri dönüyordu. Artık sayım yaymadan bağımsızdır (independent of emission). Bu, tek bir soru sorabileceğiniz, bu çalışma kitabının (workbook) gerçekte (actually) kaç dolu hücre içerdiğini sorabileceğiniz ve yanıtı (answer) hiçbir geri çağrısı (callbacks) olmayan bir tarama (scan) fiyatına alabileceğiniz anlamına gelir (means). ReadFile ve ReadStream bu toplamı bir Int64 olarak döndürür ve aynı (same) numara (number) sonradan CellCount özelliği (property) olarak mevcuttur. -1 dönüşü (return), dosyanın açılamadığını veya bir OOXML paketi olmadığını belirtir
var
Reader: TXLSDirectReader;
Populated: Int64;
begin
Reader := TXLSDirectReader.Create;
try
// No OnCell handler: a pure populated-cell census, still near-constant memory
Populated := Reader.ReadFile('quarterly_export.xlsx');
if Populated < 0 then
raise Exception.Create('Not a readable XLSX package')
else
Writeln(Format('%d populated cells (CellCount = %d)',
[Populated, Reader.CellCount]));
finally
Reader.Free;
end;
end;
Tam (full) tarama için, işleyiciyi bağlar (attach the handler) ve ReadFile işlevini tıpkı (exactly) aynı yolla çağırırsınız. Tam bir yüklemeyle olan zıtlık meselenin ta kendisidir (the whole point): quarterly_export.xlsx (quarterly_export.xlsx) dosyasını bir çalışma kitabına yüklemek, her hücreyi yerleşik (resident) bir nesneye genişletecek (expand) ve tüm grubu (the lot) tutacakken, doğrudan (direct) okuyucu yalnızca paylaşılan (shared) dizeleri (strings) ve stil tablosunu tutar (keeps) ve on iki milyon hücre her defasında bir tane olmak üzere OnCell'inizden geçer. Hücre başına çalışan aritmetik geride hiçbir şey bırakmaz, dolayısıyla en yüksek (peak) bellek, satır sayısına göre değil çalışma kitabının farklı dize (distinct-string) sayısına (count) göre belirlenir (set)
Görev, büyük bir çalışma kitabını bir kez okumak ve onu ayıklamak veya özetlemek olduğunda doğrudan okuyucu doğru araçtır. Bunun yerine tam modelin (full model) rastgele erişimine ihtiyaç duyduğunuzda ancak büyük dosyalarda davranmasını (behave) istediğinizde, Delphi'deki büyük çalışma kitabı performansı hakkındaki notlarımızdaki ayarlama (tuning) bu yolu (path) kapsar. Ve büyük bir çıktı (output) üretmek üzere yön (direction) tersine çevrildiğinde, sunucu toplu işleri (server batch jobs) için akışlı yazma (streaming-write) kılavuzu, yazma işlemi için aynı sabit bellek (constant-memory) disiplinini uygular. Üçü de bu blogda (blog) başka yerlerde ele alınan okuma, yazma, formül (formula) ve format (formatting) API'lerinin yanında Delphi ve C++Builder için HotXLS Bileşeninin bir parçası (part) olarak sevk edilir (ship)