Зіткнувся з неправильною сумою у футері DevExpress: він показував 1,00 замість 11,00. Помилка з’являлася лише на частині даних і нагадувала проблему відмальовування: наче в числа зникла перша цифра. І це не вперше — періодично натрапляв на неї ще кілька років тому, але оновлення під старезну Delphi 7 не знайшов. У результаті навіть писав обхід бага з ручним підсумовуванням і виведенням у вигляді тексту. Про це я вже писав раніше у Шпаргалці з DevExpress cxGrid і Delphi 7 (російською). Тепер же вирішив: раптом ШІ знайде корінь проблеми. Усі, хто використовує DevExpress, знають, наскільки це монументальна споруда: тисячі файлів вихідного коду, у яких можна копирсатися дуже довго. Однак, на мій подив, ШІ, покопавшись у них, дійшов висновку, що помилка глибше — у базових компонентах Delphi 7, а саме у встановленій RTL Delphi 7. Виправлення виявилося невеликим: змінили два рядки й перевірили результат як окремо від інтерфейсу, так і на справжніх формах.
Мінімальний приклад без бази та компонентів
Для відтворення достатньо консольного проєкту Delphi 7 і стандартного модуля FMTBcd. Нулі в дробовій частині рядків залишені навмисно: вони задають масштаб, на якому виявився дефект.
program BcdFooterRepro;
{$APPTYPE CONSOLE}
uses
SysUtils, Variants, FMTBcd;
function DecimalValue(const S: string): Variant;
begin
Result := VarFMTBcdCreate(
StrToBcd(StringReplace(S, '.', DecimalSeparator, [])));
end;
var
Total: Variant;
begin
Total := DecimalValue('4.00000');
Total := Total + DecimalValue('5.00000');
Writeln('4 + 5 = ', VarToStr(Total));
Total := Total + DecimalValue('2.00000');
Writeln('4 + 5 + 2 = ', VarToStr(Total));
end.
З дефектною бібліотекою другий результат — 1; після виправлення — 11.
Де втрачався розряд
Ланцюжок обчислення виявився таким:
штатне підсумовування DevExpress → додавання BCD Variant → TFMTBcdData.DoAdd → BcdAdd → NormalizeBcdPair
BCD зберігає десяткові цифри. Precision описує загальну точність подання, а масштаб — кількість дробових розрядів. Для 9.00000 за точності 6 і масштабу 5 залишається один розряд цілої частини. Для 11.00000 потрібен уже другий. Перед додаванням BcdAdd викликав:
NormalizeBcdPair(bcdIn1, bcdIn2, bcd1, bcd2, 1);
Останній аргумент, ExtraDigits, запитував запас для перенесення. Однак у дослідженій версії NormalizeBcdPair цей аргумент не враховувався під час вибору потрібної точності. Операнди вміщалися у вибране подання, а результат із перенесенням — уже ні. У нашому прикладі старший розряд втрачався. Це також пояснює, чому помилка здавалася випадковою. Вона залежала від масштабу, доступної точності та появи перенесення за конкретної послідовності додавань.
Патч: два рядки
У NormalizeBcdPair замінили умову збільшення точності. Решта алгоритму залишилася незмінною:
while (MaxDigits < (SizeOf(Bcd1.Fraction) * 2)) and
- ((MaxDigits - MaxScale < Bcd1.Precision - BcdScale(Bcd1)) or
- (MaxDigits - MaxScale < Bcd2.Precision - BcdScale(Bcd2))) do
+ ((MaxDigits - MaxScale < NumberOfDigits(Bcd1) - BcdScale(Bcd1) + ExtraDigits) or
+ (MaxDigits - MaxScale < NumberOfDigits(Bcd2) - BcdScale(Bcd2) + ExtraDigits)) do
Inc(MaxDigits, 2);
Тепер перевірка враховує і фактично потрібні цифри операндів, і додатковий розряд, запитаний функцією, що її викликає. NumberOfDigits тут важливий: якщо за кожного додавання додавати запас до вже виділеної точності, вона може зростати навіть без збільшення самого числа. Підсумок із тисяч рядків потребує перевірки й цього сценарію. Ми зберегли BCD-арифметику. Перетворювати значення на Double, змінювати SQL або підміняти штатне підсумовування користувацьким обробником не знадобилося. DevExpress також залишився без змін. Точний патч FMTBcd.patch і скрипт його застосування Apply-FmtBcdFix.ps1 збережені окремо. Повторне збирання має оновити два файли:
Delphi7/Lib/FMTBcd.dcu Delphi7/Lib/Debug/FMTBcd.dcu
Так вирішилася проблема, що дошкуляла багато років. З цікавості я пошукав, чи є в когось цей патч, але саме такого виправлення ніде не знайшов. Зате подивився, що ще випускають ентузіасти для старої Delphi.
| Проєкт | Призначення та спосіб підключення |
|---|---|
| VCL Fix Pack 1.4, Andreas Hausladen | Виправлення VCL і RTL під час запуску застосунку. Підтримує Delphi 7; серед виправлень — фокус MDI-вікон, контекстні меню та відмальовування деяких елементів керування. Підключається одним модулем до проєкту. |
| Synopse Enhanced RTL, Arnaud Bouchez | Виправлення й оптимізації вихідного коду для Delphi 7 і 2007, зокрема базових модулів RTL і деяких VCL-модулів. Ширше втручання в бібліотеку, ніж окремий модуль, що підключається. |
| FastCode | Оптимізовані заміни окремих функцій RTL: обробки рядків, пошуку, порівняння та роботи з пам’яттю. Основне завдання — продуктивність. |
| DelphiSpeedUp 3.1 | Прискорення та виправлення середовища розробки, зокрема для Delphi 7. Це окремий рівень: поліпшення IDE не слід вважати виправленнями арифметики зібраного застосунку. |
Тобто якось оновлювати старі бібліотеки можна, але «наші лави рідшають» — ці патчі теж уже доволі давно не оновлювалися. З них я собі встановив DelphiSpeedUp 3.1 і FastMM4.
FastMM4 як поліпшення стандартного середовища виконання
FastMM4 — сторонній менеджер пам’яті Pierre le Riche. Формально це заміна менеджера пам’яті, а не пакет виправлень VCL. Але для старого проєкту він виконує схожу практичну роль: поліпшує базовий механізм середовища виконання, не вимагаючи переписувати прикладний код. Він обслуговує виділення, звільнення та перерозподіл пам’яті, розрахований на багатопотокову роботу й зменшення фрагментації. Доступні засоби діагностики витоків і помилок пам’яті, поведінка яких залежить від конфігурації. Автор указує підтримку Delphi 4 і новіших версій, включно з Delphi 7. Опис проєкту. Останнє оновлення — від 10 серпня 2021 року. Про цей прискорювач я вже колись згадував у Шпаргалці з DevExpress cxGrid і Delphi 7 (російською).
