Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,7 @@ cython_debug/

ignore
m4ri-release-*
# setup.py downloads m4ri-{release}/ on build
m4ri-*/
# local uv lockfile
uv.lock
91 changes: 91 additions & 0 deletions benchmarks/mt_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import argparse
import json
import random
import statistics
from time import perf_counter

from gf2bv import LinearSystem
from gf2bv.crypto.mt import MT19937


def run_mt_case(bs: int, samples: int | None = None) -> dict[str, float | bool | int]:
rand = random.Random(3142)
state = tuple(rand.getstate()[1][:-1])

effective_bs = ((bs - 1) & bs) or bs
samples = 624 * 32 // effective_bs if samples is None else samples
outputs = [rand.getrandbits(bs) for _ in range(samples)]

lin = LinearSystem([32] * 624)
mt = lin.gens()
rng = MT19937(mt)

build_start = perf_counter()
zeros = [rng.getrandbits(bs) ^ out for out in outputs] + [mt[0] ^ 0x80000000]
build_end = perf_counter()
solution = lin.solve_one(zeros)
solve_end = perf_counter()

return {
"build_seconds": build_end - build_start,
"solve_seconds": solve_end - build_end,
"total_seconds": solve_end - build_start,
"samples": samples,
"ok": solution == state,
}


CASES: dict[str, tuple[int, int | None]] = {
"mt32": (32, None),
"mt17": (17, None),
"mt9": (9, None),
"mt1": (1, None),
"mt1337": (1337, 19968 // 1337 + 10),
"mt137": (137, 19968 // 137 + 60),
}


def summarize(case_name: str, runs: list[dict[str, float | bool | int]]) -> dict[str, object]:
if not all(run["ok"] for run in runs):
raise AssertionError(f"{case_name} produced an incorrect solution")

build = [float(run["build_seconds"]) for run in runs]
solve = [float(run["solve_seconds"]) for run in runs]
total = [float(run["total_seconds"]) for run in runs]

return {
"runs": runs,
"summary": {
"build_mean": statistics.fmean(build),
"build_median": statistics.median(build),
"solve_mean": statistics.fmean(solve),
"solve_median": statistics.median(solve),
"total_mean": statistics.fmean(total),
"total_median": statistics.median(total),
},
}


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--runs", type=int, default=10)
parser.add_argument(
"--case",
dest="cases",
action="append",
choices=sorted(CASES),
help="Benchmark only the selected case. Repeat the flag to include multiple cases.",
)
args = parser.parse_args()

selected_cases = args.cases or list(CASES)
results: dict[str, object] = {"runs": args.runs, "cases": {}}
for case_name in selected_cases:
bs, samples = CASES[case_name]
case_runs = [run_mt_case(bs, samples) for _ in range(args.runs)]
results["cases"][case_name] = summarize(case_name, case_runs)
print(json.dumps(results, indent=2, sort_keys=True))


if __name__ == "__main__":
main()
4 changes: 1 addition & 3 deletions gf2bv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,9 @@ def _solve_internal(self, zeros: Zeros, mode: TSolveMode):
# no solution
return
cols = self._cols
if cols > len(eqs):
# m4ri_solve requires rows >= cols, pad with zeros
eqs += [0] * (cols - len(eqs))
# all mode (mode == 1): may return None if no solution, otherwise return an iterator
# one solution mode (mode == 0): return the solution directly if it exists, otherwise return None
# the native solver pads missing rows with zeros internally
return m4ri_solve(eqs, cols, mode)

def _convert_sol(self, s: int) -> tuple[int, ...]:
Expand Down
Loading