Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CAPACITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,10 @@ of 2^27.
The same table, with each slot also holding the memento that lets it answer a range
query: `F + 1 + m + 3` bits for a range up to 2^m wide, so a wider range costs more per
slot. The items counted are entries, one for each block of `range` consecutive keys, so
keys that fall in blocks already present add nothing. A range of 256 reads back a table of
2^27 slots, and a range of 65,536 only 2^26. A slot is held in a 64-bit word, so the
constructor refuses a fingerprint and a range that make it wider.
keys that fall in blocks already present add nothing. A read holds each table twice, in the
payload and in the table its words are read straight into, so the default budget reads back
a table of 2^27 slots at a range of 256 and at a range of 65,536. A slot is held in a 64-bit
word, so the constructor refuses a fingerprint and a range that make it wider.

## Retains k

Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
widest table an array holds, 2^31 slots of 63 bits, is still taken and priced for its words.
No writer wrote such a table: the constructor and every doubling stop short of it.

- **A `MementoFilter` read holds each table once, and is charged for it once.** Its reader
read a table's words a word at a time into an array of their own and then copied them
into the table the segment builds, so a read held, and was charged for, every table
twice, as an `InfiniFilter` read did before #313. The words are now read straight into
the table, in pieces through a 16 KiB buffer on the stack, as an `InfiniFilter`'s are,
and the price and the charges are one table each and the segment, so they still come to
the same sum. The segment and its codec are charged as two small objects, 128 bytes,
where they were charged as one beside the second table: measured in a no-GC region they
are 120 bytes, more than the one. Measured in a no-GC region, a filter of 2-bit
fingerprints whose chain holds 110,672 bytes of tables allocated 333,024 bytes to read
and now allocates 222,352, and in the large form, where the payload is not held, 222,424
and now 111,752. So the default budget now reads a filter of 8-bit fingerprints at a
range of 65,536 up to a table of 2^27 slots, 100,663,296 items, where it stopped at 2^26;
at a range of 256 it still stops at 2^27. A payload claiming a table no array holds is
still refused for that before anything is priced or allocated. (#327)

- **An `InfiniFilter` read holds each table once, and is charged for it once.** Its reader
read a table's words into an array of their own and then copied them into the table the
segment builds, so a read held, and was charged for, every table twice: a table of 2^32
Expand Down
3 changes: 1 addition & 2 deletions FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,7 @@ gigabytes, and loads only with a budget raised to match. What each read is charg
| `CountMinSketch`, `CountSketch`, `PrivateCountMinSketch`, `SublimeCountMinSketch` | every row, before it is read; Sublime also its packed rows |
| `TopK`, `HeavyKeeper` | a heap sized by the elements held, not by `k`; HeavyKeeper also its bucket arrays |
| `ThetaSketch`, `TupleSketch`, `MinHashSignature`, `Grafite`, `SetSketch`, `InvertibleBloomLookupTable`, `DDSketch`, `HyperLogLogPlus` (sparse) | arrays sized by the values the payload carries |
| `InfiniFilter` | each table, its words read straight into the segment |
| `MementoFilter` | each table twice: as read, and as the segment built from it |
| `InfiniFilter`, `MementoFilter` | each table, its words read straight into the segment |
| `DpswSketch` | its checkpoints, rebuilt from the substream size, and a list of segments per substream; each segment's sketch as above |

## Payloads
Expand Down
33 changes: 16 additions & 17 deletions ProbabilisticDataStructures/MementoFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -839,21 +839,19 @@ private static MementoFilter ReadPayload(
// has the same width, and if a payload says otherwise the lowest limit holds.
filter.QuotientBitsLimit = Math.Min(filter.QuotientBitsLimit, QuotientBitsLimitFor(slotBits));

// Read into one array, then copied into the table the segment builds.
// The segment's table is allocated, and the words, which have no length before
// them, are read straight into it. Read a word at a time into an array of their
// own and copied into the table, every table was held, and charged, twice
// (#327). The shape's check, above, holds the words to what one array holds.
budget.Charge(SegmentCost(words));
var data = new ulong[words];
var anyBitSet = false;
for (var w = 0; w < words; w++)
{
data[w] = reader.ReadUInt64();
anyBitSet |= data[w] != 0;
}
var segment = MementoSegment.Restore(quotientBits, fingerprintBits, mementoBits, count);
reader.ReadWords(segment.Data);

// The one table with a history. A slot of exactly 64 bits was written with
// a mask of zero, so such a table counted every key it was given and stored
// none: its count is positive and its words are all zero, which no table
// that holds an entry can be, since an entry sets its slot's metadata.
if (slotBits == MementoSegment.MaxSlotBits && count > 0 && !anyBitSet)
if (slotBits == MementoSegment.MaxSlotBits && count > 0 && !segment.Data.AsSpan().ContainsAnyExcept(0UL))
{
throw new InvalidDataException(
$"Table {i} counts {count} entries and every word of it is zero. " +
Expand All @@ -868,8 +866,6 @@ private static MementoFilter ReadPayload(
// since decoding a run walks it, and then what a run says about its own boxes,
// once for each occupied run, so that a box the codec cannot decode is refused
// now rather than by the first query that reads it.
var segment = MementoSegment.Restore(
quotientBits, fingerprintBits, mementoBits, count, data);
segment.CheckMetadata(i);
segment.CheckRuns(i);
filter.Segments.Add(segment);
Expand Down Expand Up @@ -903,9 +899,8 @@ private static void CheckHash(HashId recorded, Func<ReadOnlySpan<byte>, ulong>?
}

/// <summary>
/// Prices what <see cref="Read"/> allocates beyond the envelope: each table's
/// words, read into one array and copied into the table. See
/// <see cref="PayloadPrice"/>.
/// Prices what <see cref="Read"/> allocates beyond the envelope: each table, which its
/// words are read straight into. See <see cref="PayloadPrice"/>.
/// </summary>
internal static void Price(ref PayloadReader reader, ushort version)
{
Expand All @@ -929,9 +924,13 @@ internal static void Price(ref PayloadReader reader, ushort version)
}
}

/// <summary>One table's words, read into one array and copied into the table.</summary>
private static long SegmentCost(uint words) => ReadBudget.Sum(
ReadBudget.Times(2, ReadBudget.ArrayOf(words, sizeof(ulong))),
/// <summary>
/// One table, its words read straight into it, and the segment that holds it with its
/// codec, which come to more than one small object allows for, so are charged as two.
/// </summary>
internal static long SegmentCost(uint words) => ReadBudget.Sum(
ReadBudget.ArrayOf(words, sizeof(ulong)),
ReadBudget.SmallObject,
ReadBudget.SmallObject);

private static void CheckChain(int mementoBits, uint segmentCount)
Expand Down
17 changes: 5 additions & 12 deletions ProbabilisticDataStructures/MementoSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,12 @@ private void Shape(int quotientBits, int fingerprintBits, int mementoBits)
}

/// <summary>
/// Restores a segment from stored fields without rebuilding its table.
/// A segment of a stored shape and count, its table allocated and every word of it
/// nought, for the reader to read the stored words straight into (#327), and then to
/// check its metadata and its runs.
/// </summary>
internal static MementoSegment Restore(
int quotientBits, int fingerprintBits, int mementoBits, uint count,
ulong[] data)
{
var segment = new MementoSegment(quotientBits, fingerprintBits, mementoBits)
{
Count = count,
};
data.CopyTo(segment.Data, 0);
return segment;
}
internal static MementoSegment Restore(int quotientBits, int fingerprintBits, int mementoBits, uint count) =>
new(quotientBits, fingerprintBits, mementoBits) { Count = count };

/// <summary>
/// Empties the table where it is, leaving it as a new one of its shape would be: no
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ building at the scale it describes, which is the measurement as of commit 426348
| `InfiniFilter` | items added, 8-bit fingerprints | 6,442,450,944 | 201,326,592 | 805,306,368 |
| `InfiniFilter` | items added, 16-bit fingerprints | 3,221,225,472 | 100,663,296 | 402,653,184 |
| `MementoFilter` | items added, one per block, range 256, 8-bit fingerprints | 1,610,612,736 | 100,663,296 | 402,653,184 |
| `MementoFilter` | items added, one per block, range 65,536, 8-bit fingerprints | 1,610,612,736 | 50,331,648 | 402,653,184 |
| `MementoFilter` | items added, one per block, range 65,536, 8-bit fingerprints | 1,610,612,736 | 100,663,296 | 402,653,184 |

#### Retains k

Expand Down
16 changes: 16 additions & 0 deletions TestProbabilisticDataStructures/CapacityClaims.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,22 @@ static bool ReadsUpTo(int f, int q) =>
return SlotBits(16) == 20 && SlotBits(8) == 12 && ReadsUpTo(8, 28) && ReadsUpTo(16, 27);
}),
new Claim("`F + 1 + m + 3` bits", () => ChargeLine.Known.ContainsKey("MementoFilter 256")),
// In bytes a word of the table, as Infini's: eight for the payload's copy of the
// word, and eight for the table it is read into, which it once was twice (#327).
new Claim("A read holds each table twice, in the payload and in the table its words are read straight into", () =>
Near(Slope("MementoFilter 256"), 16, 0.01) && Near(Slope("MementoFilter 65536"), 16, 0.01)),
new Claim("so the default budget reads back a table of 2^27 slots at a range of 256 and at a range of 65,536", () =>
{
static bool ReadsUpTo(ulong range, int q)
{
var slotBits = 8 + 1 + (int)Math.Ceiling(Math.Log2(range)) + 3;
var line = ChargeLine.Known[$"MementoFilter {range}"];
return line.Surely((ulong)MementoSegment.Words(1u << q, slotBits))
&& !line.Possibly((ulong)MementoSegment.Words(1u << (q + 1), slotBits));
}

return ReadsUpTo(256, 27) && ReadsUpTo(65536, 27);
}),

// ---- Inverse and the retaining sketches -----------------------------------------
new Claim("reads back at 8 bytes a slot, and a full one at 56", () =>
Expand Down
6 changes: 4 additions & 2 deletions TestProbabilisticDataStructures/TestExpandingTableLimits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,11 @@ public void TestAMementoTableNoArrayHoldsIsRefusedByNameBeforeAnythingIsPricedOr
{
problems.Add($"2^31 slots of 63 bits: the price did not walk the table an array holds: {fitting.Stopped.Message}");
}
else if (fitting.Priced < 2 * fits.Words * sizeof(ulong))
else if (fitting.Priced < fits.Words * sizeof(ulong) || fitting.Priced >= 2 * fits.Words * sizeof(ulong))
{
problems.Add($"2^31 slots of 63 bits: the price came to {fitting.Priced:N0} bytes, less than the table's words twice over");
// Once: the words are read straight into the table (#327), where they were
// read into an array of their own and copied, and priced twice over.
problems.Add($"2^31 slots of 63 bits: the price came to {fitting.Priced:N0} bytes, not the table's words once");
}

var (fitsThrown, fitsAllocated, _) = Read(fits, ReadOptions.Default);
Expand Down
56 changes: 56 additions & 0 deletions TestProbabilisticDataStructures/TestLargeEnvelopeReading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,62 @@ public void TestACountMinSketchsCountersAreReadFromTheLargeFormInPiecesStraightI
}
}

/// <summary>
/// A <see cref="MementoFilter"/>'s table, whose words have no length before them, is read
/// from the large form in pieces straight into its segment's table (#327), as an Infini
/// filter's is (#313): through the stream's span overload, every piece as long as a piece
/// but the last, one after another from the table's first byte to its last, and nothing
/// else read among them. Shown with the piece as it ships and lowered to eight words.
/// </summary>
/// <remarks>
/// Before #327 the words were read a word at a time into an array of their own, eight
/// bytes asked of the stream and appended to the checksum for each, and then copied into
/// the table. The table is found as the bytes its words are written as, which nothing
/// else in the payload repeats; the budget sample is one table longer than a piece as it
/// ships, so it is read in more than one at either length.
/// </remarks>
[TestMethod]
public void TestAMementoFiltersTableIsReadFromTheLargeFormInPiecesStraightIntoItsSegment()
{
var sample = TestReadBudget.Samples.Value[StructureId.MementoFilter];
var large = TestReadBudget.InTheLargeForm(sample).Bytes;
var filter = (MementoFilter)sample.Read(sample.Bytes, ReadOptions.Unlimited);
Assert.HasCount(1, filter.Segments, "premise: the sample is not one table");

var words = filter.Segments[0].Data;
var table = new byte[words.Length * sizeof(ulong)];
for (var w = 0; w < words.Length; w++)
{
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(w * sizeof(ulong)), words[w]);
}

long at = large.AsSpan().IndexOf(table);
Assert.IsGreaterThanOrEqualTo(0L, at, "the table is not in the payload in the large form");
Assert.AreEqual(at, (long)large.AsSpan().LastIndexOf(table), "the table appears in the payload twice");
Assert.IsGreaterThan((long)ShippedBlockPiece, (long)table.Length,
$"the table's {table.Length:N0} bytes are no longer than a piece as it ships");
var end = at + table.Length;

foreach (var piece in new int?[] { null, Piece })
{
var length = (long)(piece ?? ShippedBlockPiece);
var reads = SecondPass(sample, large, piece).Where(a => a.From < end && at < a.To).ToList();
var expected = new List<Asked>();
for (var from = at; from < end; from += length)
{
expected.Add(new Asked(false, from, Math.Min(from + length, end), false));
}

Console.WriteLine(
$"[TABLE] MementoFilter: {table.Length:N0} bytes at {at:N0}, piece {length:N0}: " +
$"{reads.Count:N0} reads, the first {reads[0].Count:N0} bytes");
CollectionAssert.AreEqual(expected, reads,
$"the table's {table.Length:N0} bytes at {at:N0}, with the piece at {length:N0}, were read in " +
$"{reads.Count:N0} reads, the first of {reads[0].Count:N0} bytes through the " +
$"{(reads[0].Array ? "array" : "span")} overload, and not in {expected.Count:N0} pieces through the span overload");
}
}

/// <summary>
/// An <see cref="InfiniFilter"/>'s table, whose words have no length before them, is read
/// from the large form in pieces straight into its segment's table (#313), as a count-min
Expand Down
Loading
Loading