Skip to content

Commit d5f39c8

Browse files
authored
Remove the use of floating-point for palette packing (#1565)
This is primarily a correctness change, *not* a performance one. The expected performance impact is minimal anyway. The goal is to eliminate the use of platform-inconsistent floating-point operations for this load-bearing task.
1 parent a5d18d6 commit d5f39c8

File tree

1 file changed

+55
-29
lines changed

1 file changed

+55
-29
lines changed

src/gfx/pal_packing.cpp

+55-29
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
#include <algorithm>
66
#include <deque>
77
#include <inttypes.h>
8+
#include <numeric>
89
#include <optional>
910
#include <queue>
11+
#include <stdint.h>
1012
#include <type_traits>
1113
#include <unordered_set>
14+
#include <utility>
1215

1316
#include "helpers.hpp"
1417

@@ -199,21 +202,44 @@ class AssignedProtos {
199202
return colors.size() <= options.maxOpaqueColors();
200203
}
201204

205+
// The `relSizeOf` method below should compute the sum, for each color in `protoPal`, of
206+
// the reciprocal of the "multiplicity" of the color across "our" proto-palettes.
207+
// However, literally computing the reciprocals would involve floating-point division, which
208+
// leads to imprecision and even platform-specific differences.
209+
// We avoid this by multiplying the reciprocals by a factor such that division always produces
210+
// an integer; the LCM of all values the denominator can take is the smallest suitable factor.
211+
static constexpr uint32_t scaleFactor = [] {
212+
// Fold over 1..=17 with the associative LCM function
213+
// (17 is the largest the denominator in `relSizeOf` below can be)
214+
uint32_t factor = 1;
215+
for (uint32_t n = 2; n <= 17; ++n) {
216+
factor = std::lcm(factor, n);
217+
}
218+
return factor;
219+
}();
220+
202221
/*
203-
* Computes the "relative size" of a proto-palette on this palette
222+
* Computes the "relative size" of a proto-palette on this palette;
223+
* it's a measure of how much this proto-palette would "cost" to introduce.
204224
*/
205-
double relSizeOf(ProtoPalette const &protoPal) const {
225+
uint32_t relSizeOf(ProtoPalette const &protoPal) const {
206226
// NOTE: this function must not call `uniqueColors`, or one of its callers will break!
207-
double relSize = 0.;
227+
228+
uint32_t relSize = 0;
208229
for (uint16_t color : protoPal) {
209-
auto n = std::count_if(RANGE(*this), [this, &color](ProtoPalAttrs const &attrs) {
210-
ProtoPalette const &pal = (*_protoPals)[attrs.protoPalIndex];
211-
return std::find(RANGE(pal), color) != pal.end();
212-
});
213-
// NOTE: The paper and the associated code disagree on this: the code has
214-
// this `1 +`, whereas the paper does not; its lack causes a division by 0
215-
// if the symbol is not found anywhere, so I'm assuming the paper is wrong.
216-
relSize += 1. / (1 + n);
230+
auto multiplicity = // How many of our proto-palettes does this color also belong to?
231+
std::count_if(RANGE(*this), [this, &color](ProtoPalAttrs const &attrs) {
232+
ProtoPalette const &pal = (*_protoPals)[attrs.protoPalIndex];
233+
return std::find(RANGE(pal), color) != pal.end();
234+
});
235+
// We increase the denominator by 1 here; the reference code does this,
236+
// but the paper does not. Not adding 1 makes a multiplicity of 0 cause a division by 0
237+
// (that is, if the color is not found in any proto-palette), and adding 1 still seems
238+
// to preserve the paper's reasoning.
239+
//
240+
// The scale factor should ensure integer divisions only.
241+
assume(scaleFactor % (multiplicity + 1) == 0);
242+
relSize += scaleFactor / (multiplicity + 1);
217243
}
218244
return relSize;
219245
}
@@ -383,18 +409,18 @@ std::tuple<DefaultInitVec<size_t>, size_t>
383409
size_t bestPalIndex = assignments.size();
384410
// We're looking for a palette where the proto-palette's relative size is less than
385411
// its actual size; so only overwrite the "not found" index on meeting that criterion
386-
double bestRelSize = protoPal.size();
412+
uint32_t bestRelSize = protoPal.size() * AssignedProtos::scaleFactor;
387413

388414
for (size_t i = 0; i < assignments.size(); ++i) {
389415
// Skip the page if this one is banned from it
390416
if (attrs.isBannedFrom(i)) {
391417
continue;
392418
}
393419

394-
double relSize = assignments[i].relSizeOf(protoPal);
420+
uint32_t relSize = assignments[i].relSizeOf(protoPal);
395421
options.verbosePrint(
396422
Options::VERB_TRACE,
397-
" Relative size to palette %zu (of %zu): %.20f (size = %zu)\n",
423+
" Relative size to palette %zu (of %zu): %" PRIu32 " (size = %zu)\n",
398424
i,
399425
assignments.size(),
400426
relSize,
@@ -444,23 +470,23 @@ std::tuple<DefaultInitVec<size_t>, size_t>
444470
ProtoPalette const &rhsProtoPal = protoPalettes[rhs.protoPalIndex];
445471
size_t lhsSize = lhsProtoPal.size();
446472
size_t rhsSize = rhsProtoPal.size();
447-
double lhsRelSize = bestPal.relSizeOf(lhsProtoPal);
448-
double rhsRelSize = bestPal.relSizeOf(rhsProtoPal);
473+
uint32_t lhsRelSize = bestPal.relSizeOf(lhsProtoPal);
474+
uint32_t rhsRelSize = bestPal.relSizeOf(rhsProtoPal);
449475

450-
// This comparison is algebraically equivalent to
451-
// `lhsSize / lhsRelSize < rhsSize / rhsRelSize`,
452-
// but without potential precision loss from floating-point division.
453476
options.verbosePrint(
454477
Options::VERB_TRACE,
455-
" Proto-palettes %zu <=> %zu: Efficiency: %zu / %.20f <=> %zu / "
456-
"%.20f\n",
478+
" Proto-palettes %zu <=> %zu: Efficiency: %zu / %" PRIu32 " <=> %zu / "
479+
"%" PRIu32 "\n",
457480
lhs.protoPalIndex,
458481
rhs.protoPalIndex,
459482
lhsSize,
460483
lhsRelSize,
461484
rhsSize,
462485
rhsRelSize
463486
);
487+
// This comparison is algebraically equivalent to
488+
// `lhsSize / lhsRelSize < rhsSize / rhsRelSize`,
489+
// but without potential precision loss from floating-point division.
464490
return lhsSize * rhsRelSize < rhsSize * lhsRelSize;
465491
}
466492
);
@@ -471,23 +497,23 @@ std::tuple<DefaultInitVec<size_t>, size_t>
471497
ProtoPalette const &maxProtoPal = protoPalettes[maxEfficiencyIter->protoPalIndex];
472498
size_t minSize = minProtoPal.size();
473499
size_t maxSize = maxProtoPal.size();
474-
double minRelSize = bestPal.relSizeOf(minProtoPal);
475-
double maxRelSize = bestPal.relSizeOf(maxProtoPal);
476-
// This comparison is algebraically equivalent to
477-
// `maxSize / maxRelSize - minSize / minRelSize < .001`,
478-
// but without potential precision loss from floating-point division.
479-
// TODO: yikes for float comparison! I *think* this threshold is OK?
500+
uint32_t minRelSize = bestPal.relSizeOf(minProtoPal);
501+
uint32_t maxRelSize = bestPal.relSizeOf(maxProtoPal);
480502
options.verbosePrint(
481503
Options::VERB_TRACE,
482-
" Proto-palettes %zu <= %zu: Efficiency: %zu / %.20f <= %zu / %.20f\n",
504+
" Proto-palettes %zu <= %zu: Efficiency: %zu / %" PRIu32 " <= %zu / %" PRIu32
505+
"\n",
483506
minEfficiencyIter->protoPalIndex,
484507
maxEfficiencyIter->protoPalIndex,
485508
minSize,
486509
minRelSize,
487510
maxSize,
488511
maxRelSize
489512
);
490-
if (maxSize * minRelSize - minSize * maxRelSize < minRelSize * maxRelSize * .001) {
513+
// This comparison is algebraically equivalent to
514+
// `maxSize / maxRelSize == minSize / minRelSize`,
515+
// but without potential precision loss from floating-point division.
516+
if (maxSize * minRelSize == minSize * maxRelSize) {
491517
options.verbosePrint(Options::VERB_TRACE, " All efficiencies are identical\n");
492518
break;
493519
}

0 commit comments

Comments
 (0)