Skip to content

Commit 3cf627a

Browse files
authored
Merge pull request #1 from adriencaccia/perf/adriencaccia
perf improvements for adriencaccia
2 parents 72790a0 + de6ea69 commit 3cf627a

4 files changed

Lines changed: 92 additions & 11 deletions

File tree

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.15t
1+
3.15

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Python Performance Lab: Sharpening Your Instincts
1+
# Python Performance Lab: Sharpening Your Instincts - `adriencaccia`
22

33
A PyCon US 2026 hands-on tutorial. You optimize intentionally slow Python code
44
across three rounds plus a team challenge, measuring every change with

rounds/1_histogram/solution.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,21 @@
88

99
def compute_histogram(path: str) -> dict[bytes, int]:
1010
"""Frequency of every 2-byte bigram in the file at ``path``."""
11-
# TODO: remove this delegation and write your own implementation here.
12-
from .baseline import compute_histogram as _baseline
11+
# Step 1: read the whole file into memory as a single bytes object.
12+
with open(path, "rb") as f:
13+
data = f.read()
1314

14-
return _baseline(path)
15+
# Create a 2D matrix to count bigrams
16+
counts = [[0] * 256 for _ in range(256)]
17+
18+
for i in range(len(data) - 1):
19+
# Increment the count in each cell
20+
counts[data[i]][data[i + 1]] += 1
21+
22+
# Convert the matrix to the original format
23+
output = {}
24+
for i in range(256):
25+
for j in range(256):
26+
if counts[i][j] > 0:
27+
output[bytes([i, j])] = counts[i][j]
28+
return output

rounds/3_dna/solution.py

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,80 @@
55
own faster implementation.
66
"""
77

8-
from .baseline import find_matches as _baseline
8+
from __future__ import annotations
9+
10+
import os
11+
from concurrent.futures import ThreadPoolExecutor
12+
13+
_NL = 0x0A # b"\n"
914

1015

1116
def find_matches(fasta_path: str, pattern: bytes) -> list[tuple[str, list[int]]]:
12-
"""Find every FASTA record whose sequence contains ``pattern``.
17+
with open(fasta_path, "rb") as f:
18+
data = f.read()
19+
20+
# Step 1: locate every record start. A record starts with ``>`` either at
21+
# offset 0 or immediately after a ``\n``.
22+
starts: list[int] = []
23+
i = 0
24+
while True:
25+
p = data.find(b">", i)
26+
if p == -1:
27+
break
28+
if p == 0 or data[p - 1] == _NL:
29+
starts.append(p)
30+
i = p + 1
31+
starts.append(len(data)) # sentinel marking the end of the last record.
32+
33+
num_records = len(starts) - 1
34+
if num_records <= 0:
35+
return []
36+
37+
# Step 2: parallel scan. Choose enough batches to keep workers balanced
38+
# even when record sizes vary.
39+
n_workers = max(1, os.cpu_count() or 1)
40+
batches = max(1, n_workers * 4)
41+
batch_size = max(1, (num_records + batches - 1) // batches)
42+
43+
def scan_batch(start_idx: int, end_idx: int) -> list[tuple[int, str, list[int]]]:
44+
out: list[tuple[int, str, list[int]]] = []
45+
for j in range(start_idx, end_idx):
46+
rec_start = starts[j]
47+
rec_end = starts[j + 1]
48+
49+
# Locate the end of the header line within this record's slice.
50+
nl = data.find(b"\n", rec_start, rec_end)
51+
if nl <= rec_start:
52+
continue # Malformed or header-only.
53+
54+
record_id = data[rec_start + 1 : nl].decode("ascii").strip()
55+
56+
# Contiguous sequence: drop the newlines so matches that straddle
57+
# line breaks are still found by ``bytes.find``.
58+
sequence = data[nl + 1 : rec_end].replace(b"\n", b"")
59+
60+
positions: list[int] = []
61+
s = 0
62+
while True:
63+
p = sequence.find(pattern, s)
64+
if p == -1:
65+
break
66+
positions.append(p)
67+
s = p + 1
68+
69+
if positions:
70+
out.append((j, record_id, positions))
71+
return out
72+
73+
with ThreadPoolExecutor(max_workers=n_workers) as pool:
74+
futures = [
75+
pool.submit(scan_batch, lo, min(lo + batch_size, num_records))
76+
for lo in range(0, num_records, batch_size)
77+
]
78+
chunks = [f.result() for f in futures]
1379

14-
Returns ``[(record_id, [positions...]), ...]`` in file order.
15-
"""
16-
# TODO: remove this delegation and write your own implementation here.
17-
return _baseline(fasta_path, pattern)
80+
# Step 3: flatten and restore file order (record index is monotonic per
81+
# batch, but batches finish in arbitrary order).
82+
flat = [item for chunk in chunks for item in chunk]
83+
flat.sort(key=lambda triple: triple[0])
84+
return [(rid, positions) for _, rid, positions in flat]

0 commit comments

Comments
 (0)