From 327079becb1ddcdc9d1a1fe5b20635af71d06326 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 12:00:28 +0200 Subject: [PATCH 1/6] Reuse counters in Acton k-nucleotide Repeated k-mers need three dictionary operations and a new boxed count. Keep one mutable counter per key so updates use one lookup and increment a native integer field. --- bench/algorithm/knucleotide/1.act | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/bench/algorithm/knucleotide/1.act b/bench/algorithm/knucleotide/1.act index 7df4b160..1f34bbcb 100644 --- a/bench/algorithm/knucleotide/1.act +++ b/bench/algorithm/knucleotide/1.act @@ -3,14 +3,27 @@ import file -def frequencies(sequence: bytes, k: int) -> dict[int, int]: - counts: dict[int, int] = {} +class Counter(object): + value: int + + def __init__(self, value: int): + self.value = value + + def increment(self): + self.value += 1 + +def frequencies(sequence: bytes, k: int) -> dict[int, Counter]: + counts: dict[int, Counter] = {} mask = (1 << (2 * k)) - 1 key = 0 for i in range(len(sequence)): key = ((key << 2) | ((sequence[i] >> 1) & 3)) & mask if i >= k - 1: - counts[key] = counts[key] + 1 if key in counts else 1 + counter = counts.get(key) + if counter is not None: + counter.increment() + else: + counts[key] = Counter(1) return counts def compress(text: str) -> int: @@ -43,12 +56,12 @@ actor main(env): sequence = b"".join(parts) for k in [1, 2]: counts = frequencies(sequence, k) - rows = [(-count, decompress(key, k)) for key, count in counts.items()] + rows = [(-count.value, decompress(key, k)) for key, count in counts.items()] for negative_count, text in sorted(rows): print("%s %.3f" % (text, -100.0 * float(negative_count) / float(len(sequence) - k + 1))) print("") for text in ["GGT", "GGTA", "GGTATT", "GGTATTTTAATT", "GGTATTTTAATTTATAGT"]: counts = frequencies(sequence, len(text)) key = compress(text) - print("%d\t%s" % (counts[key] if key in counts else 0, text)) + print("%d\t%s" % (counts[key].value if key in counts else 0, text)) env.exit(0) From a2739370f94d5fbb78e301b78fc8c09e35c9d5a1 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 12:57:27 +0200 Subject: [PATCH 2/6] Keep spectral norm arithmetic in floats Converting the denominator for every matrix element allocates a temporary integer. Keep the denominator and its increment as floats throughout each row. Both remain exact integers at the benchmark sizes. --- bench/algorithm/spectral-norm/1.act | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bench/algorithm/spectral-norm/1.act b/bench/algorithm/spectral-norm/1.act index 0ddd05c3..a3c1c7c1 100644 --- a/bench/algorithm/spectral-norm/1.act +++ b/bench/algorithm/spectral-norm/1.act @@ -7,11 +7,14 @@ def multiply(u: list[float], v: list[float], transpose: bool): n = len(u) for i in range(n): total = 0.0 - # Advancing the denominator avoids an integer multiply per element. - denominator = i * (i + 1) // 2 + (1 if transpose else i + 1) + # Integer-valued floats stay exact at the benchmark sizes and avoid + # converting the denominator for every element. + denominator = float(i * (i + 1) // 2 + (1 if transpose else i + 1)) + step = float(i + (2 if transpose else 1)) for j in range(n): - total += u[j] / float(denominator) - denominator += i + j + (2 if transpose else 1) + total += u[j] / denominator + denominator += step + step += 1.0 v[i] = total def multiply_ata(u: list[float], v: list[float], scratch: list[float]): From c5e0949e886304d4d140be1619ec6d3ac0672e5f Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 12:57:44 +0200 Subject: [PATCH 3/6] Skip covered multiples in Acton nsieve The sieve revisits composites already marked by smaller primes and marks each prime itself. Start at the square of each prime and skip marking when that square is outside the table. --- bench/algorithm/nsieve/1.act | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bench/algorithm/nsieve/1.act b/bench/algorithm/nsieve/1.act index d35ed568..b1793572 100644 --- a/bench/algorithm/nsieve/1.act +++ b/bench/algorithm/nsieve/1.act @@ -6,8 +6,9 @@ def nsieve(n: int): for i in range(2, n, 1): if flags[i]: count += 1 - for j in range(i, n, i): - flags[j] = False + if i * i < n: + for j in range(i * i, n, i): + flags[j] = False n_s = "" for j in range(0, (8-(len(str(n)))), 1): n_s += " " From a19cc7890240db12696266902fbe569b818e1fde Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 12:58:11 +0200 Subject: [PATCH 4/6] Use bytearray slices for Acton nsieve A list of boolean pointers makes the sieve table unnecessarily large. Store one byte per flag, clear composite positions with slice assignment, and count the remaining prime flags after marking. --- bench/algorithm/nsieve/1.act | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bench/algorithm/nsieve/1.act b/bench/algorithm/nsieve/1.act index b1793572..4129c37f 100644 --- a/bench/algorithm/nsieve/1.act +++ b/bench/algorithm/nsieve/1.act @@ -1,14 +1,18 @@ #!/usr/bin/env runacton def nsieve(n: int): - count = 0 - flags = [True] * n - for i in range(2, n, 1): - if flags[i]: - count += 1 - if i * i < n: - for j in range(i * i, n, i): - flags[j] = False + flags = bytearray(b"\x01" * n) + if n > 0: + flags[0] = 0 + if n > 1: + flags[1] = 0 + i = 2 + while i * i < n: + if flags[i] != 0: + start = i * i + flags[start:n:i] = b"\x00" * ((n - 1 - start) // i + 1) + i += 1 + count = flags.count(bytearray(b"\x01")) n_s = "" for j in range(0, (8-(len(str(n)))), 1): n_s += " " From 0832b75ba07eae8d7482bd4003a8b4bf783fdf80 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 12:58:37 +0200 Subject: [PATCH 5/6] Index short Acton k-mers directly Keys up to six bases have at most 4096 possible values. Count them in a small array of reusable counters instead of hashing every occurrence. Keep only observed keys in the returned table and use the existing hash table for longer keys. --- bench/algorithm/knucleotide/1.act | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bench/algorithm/knucleotide/1.act b/bench/algorithm/knucleotide/1.act index 1f34bbcb..f60b6a70 100644 --- a/bench/algorithm/knucleotide/1.act +++ b/bench/algorithm/knucleotide/1.act @@ -13,6 +13,16 @@ class Counter(object): self.value += 1 def frequencies(sequence: bytes, k: int) -> dict[int, Counter]: + if k <= 6: + # Short keys fit in at most 4096 slots, so avoid hashing each base. + counters = [Counter(0) for _ in range(1 << (2 * k))] + mask = (1 << (2 * k)) - 1 + key = 0 + for i in range(len(sequence)): + key = ((key << 2) | ((sequence[i] >> 1) & 3)) & mask + if i >= k - 1: + counters[key].increment() + return {key: counter for key, counter in enumerate(counters) if counter.value != 0} counts: dict[int, Counter] = {} mask = (1 << (2 * k)) - 1 key = 0 From d02d6f7a3e4324be44373479cdb5e50c13cc700c Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Sun, 20 Sep 2026 17:32:00 +0200 Subject: [PATCH 6/6] Explain Acton benchmark workarounds Some benchmark code exists to avoid temporary allocations in Acton. Name the counter StupidBox, update its value directly, and leave TODOs for revisiting these workarounds as the compiler improves. --- bench/algorithm/knucleotide/1.act | 21 +++++++++++---------- bench/algorithm/nsieve/1.act | 1 + bench/algorithm/spectral-norm/1.act | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bench/algorithm/knucleotide/1.act b/bench/algorithm/knucleotide/1.act index f60b6a70..2a25b90c 100644 --- a/bench/algorithm/knucleotide/1.act +++ b/bench/algorithm/knucleotide/1.act @@ -3,27 +3,28 @@ import file -class Counter(object): +# Keep the count unboxed; updating an int in a list/dict allocates today. +# TODO: Drop this workaround when Acton can update those ints unboxed. +class StupidBox(object): value: int def __init__(self, value: int): self.value = value - def increment(self): - self.value += 1 - -def frequencies(sequence: bytes, k: int) -> dict[int, Counter]: +def frequencies(sequence: bytes, k: int) -> dict[int, StupidBox]: if k <= 6: # Short keys fit in at most 4096 slots, so avoid hashing each base. - counters = [Counter(0) for _ in range(1 << (2 * k))] + # TODO: Recheck this extra path once Acton's integer hashing stops + # allocating temporary objects. + counters = [StupidBox(0) for _ in range(1 << (2 * k))] mask = (1 << (2 * k)) - 1 key = 0 for i in range(len(sequence)): key = ((key << 2) | ((sequence[i] >> 1) & 3)) & mask if i >= k - 1: - counters[key].increment() + counters[key].value += 1 return {key: counter for key, counter in enumerate(counters) if counter.value != 0} - counts: dict[int, Counter] = {} + counts: dict[int, StupidBox] = {} mask = (1 << (2 * k)) - 1 key = 0 for i in range(len(sequence)): @@ -31,9 +32,9 @@ def frequencies(sequence: bytes, k: int) -> dict[int, Counter]: if i >= k - 1: counter = counts.get(key) if counter is not None: - counter.increment() + counter.value += 1 else: - counts[key] = Counter(1) + counts[key] = StupidBox(1) return counts def compress(text: str) -> int: diff --git a/bench/algorithm/nsieve/1.act b/bench/algorithm/nsieve/1.act index 4129c37f..370ddbfb 100644 --- a/bench/algorithm/nsieve/1.act +++ b/bench/algorithm/nsieve/1.act @@ -10,6 +10,7 @@ def nsieve(n: int): while i * i < n: if flags[i] != 0: start = i * i + # TODO: Retry a plain loop once bytearray stores avoid boxing. flags[start:n:i] = b"\x00" * ((n - 1 - start) // i + 1) i += 1 count = flags.count(bytearray(b"\x01")) diff --git a/bench/algorithm/spectral-norm/1.act b/bench/algorithm/spectral-norm/1.act index a3c1c7c1..3b8b7fb1 100644 --- a/bench/algorithm/spectral-norm/1.act +++ b/bench/algorithm/spectral-norm/1.act @@ -7,8 +7,9 @@ def multiply(u: list[float], v: list[float], transpose: bool): n = len(u) for i in range(n): total = 0.0 - # Integer-valued floats stay exact at the benchmark sizes and avoid - # converting the denominator for every element. + # Integer-valued floats stay exact at the benchmark sizes. + # TODO: Return to integer arithmetic once float(int) avoids boxing: + # https://github.com/actonlang/acton/issues/3148 denominator = float(i * (i + 1) // 2 + (1 if transpose else i + 1)) step = float(i + (2 if transpose else 1)) for j in range(n):