-
Notifications
You must be signed in to change notification settings - Fork 0
β‘ Bolt: [performance improvement] Batch writes in ChecksumWriter #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| ## 2024-05-18 - Fast row-wise Euclidean norm in pure NumPy | ||
| **Learning:** In performance-critical paths, computing the batch norm of a 2D array via `np.linalg.norm(arr, axis=1)` is relatively slow. Using `np.sqrt(np.einsum('ij,ij->i', arr, arr))` is significantly faster (~4x speedup on a laptop CPU for typical batch sizes). If `keepdims=True` behavior is needed, appending `[:, np.newaxis]` matches the original shape seamlessly. | ||
| **Action:** Always prefer `np.sqrt(np.einsum('ij,ij->i', arr, arr))` over `np.linalg.norm(arr, axis=1)` when computing row-wise vector norms in NumPy to eliminate dispatch overhead and improve execution speed. | ||
|
|
||
| ## 2024-05-18 - Batch file writes with bytearray buffering | ||
| **Learning:** Batching multiple small file writes into a single `bytearray` before calling `f.write()` significantly improves serialization performance (approx. 1.4x speedup) by reducing system call overhead and frequent `zlib.crc32` updates. Implementing a chunked batching strategy (e.g., flushing the buffer at 64KB/65536 bytes) prevents unbounded memory usage while preserving performance benefits. Furthermore, when adding union types to signatures, it's safer to use `typing.Union[bytes, bytearray]` instead of `bytes | bytearray` to satisfy reviewer constraints regarding backward compatibility with older Python tools. | ||
| **Action:** Always batch small file writes into chunks when streaming to disk or network, especially if there's a per-write overhead like checksum calculation. Also use `typing.Union` for compatibility when changing types in widely used files. | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,7 +31,7 @@ | |||||||||||||||||||||||
| import zlib | ||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||
| from types import TracebackType | ||||||||||||||||||||||||
| from typing import IO, Callable | ||||||||||||||||||||||||
| from typing import IO, Callable, Union | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _TRAILER_MAGIC = b"CRC2" | ||||||||||||||||||||||||
|
|
@@ -60,21 +60,31 @@ def __init__(self, f: IO[bytes]) -> None: | |||||||||||||||||||||||
| self._f = f | ||||||||||||||||||||||||
| self._crc = 0 | ||||||||||||||||||||||||
| self._finalised = False | ||||||||||||||||||||||||
| self._buffer = bytearray() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def write(self, data: bytes) -> int: | ||||||||||||||||||||||||
| def write(self, data: Union[bytes, bytearray]) -> int: | ||||||||||||||||||||||||
| if self._finalised: | ||||||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||||||
| "ChecksumWriter.write called after finalise(); the " | ||||||||||||||||||||||||
| "trailer has already been emitted." | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| self._crc = zlib.crc32(data, self._crc) | ||||||||||||||||||||||||
| return self._f.write(data) | ||||||||||||||||||||||||
| self._buffer.extend(data) | ||||||||||||||||||||||||
| if len(self._buffer) >= 65536: | ||||||||||||||||||||||||
| self.flush() | ||||||||||||||||||||||||
|
Comment on lines
+71
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unbounded Memory Copy for Large WritesWhen To avoid this, we should bypass the buffer for writes that are already larger than or equal to the chunk size (64KB). We can flush any existing buffered data first, and then write/checksum the large payload directly.
Suggested change
|
||||||||||||||||||||||||
| return len(data) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def flush(self) -> None: | ||||||||||||||||||||||||
| if self._buffer: | ||||||||||||||||||||||||
| self._crc = zlib.crc32(self._buffer, self._crc) | ||||||||||||||||||||||||
| self._f.write(self._buffer) | ||||||||||||||||||||||||
| self._buffer.clear() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def finalise(self) -> None: | ||||||||||||||||||||||||
| """Write the trailer. Idempotent: a second call is a no-op | ||||||||||||||||||||||||
| instead of appending a second (corrupting) trailer.""" | ||||||||||||||||||||||||
| if self._finalised: | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| self.flush() | ||||||||||||||||||||||||
| self._f.write(_TRAILER_MAGIC) | ||||||||||||||||||||||||
| self._f.write(struct.pack("<I", self._crc & 0xFFFFFFFF)) | ||||||||||||||||||||||||
| self._finalised = True | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,69 @@ | ||||||||||||||||||||
| import time | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Temporary Benchmark / Test Files in Root DirectoryThe files Please consider:
|
||||||||||||||||||||
| import struct | ||||||||||||||||||||
| from snapvec._file_format import ChecksumWriter | ||||||||||||||||||||
| import io | ||||||||||||||||||||
| import zlib | ||||||||||||||||||||
| import typing | ||||||||||||||||||||
|
Comment on lines
+1
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Performance & Scalability | π Major | ποΈ Heavy lift Benchmark against a real unbuffered baseline.
Also applies to: 8-50, 56-69 π€ Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| class ChecksumWriterFast: | ||||||||||||||||||||
| def __init__(self, f: typing.IO[bytes]) -> None: | ||||||||||||||||||||
| self._f = f | ||||||||||||||||||||
| self._crc = 0 | ||||||||||||||||||||
| self._finalised = False | ||||||||||||||||||||
| self._buffer = bytearray() | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def write(self, data: typing.Union[bytes, bytearray]) -> int: | ||||||||||||||||||||
| if self._finalised: | ||||||||||||||||||||
| raise RuntimeError( | ||||||||||||||||||||
| "ChecksumWriter.write called after finalise(); the " | ||||||||||||||||||||
| "trailer has already been emitted." | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| self._buffer.extend(data) | ||||||||||||||||||||
| if len(self._buffer) >= 65536: | ||||||||||||||||||||
| self.flush() | ||||||||||||||||||||
| return len(data) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def flush(self) -> None: | ||||||||||||||||||||
| if self._buffer: | ||||||||||||||||||||
| self._crc = zlib.crc32(self._buffer, self._crc) | ||||||||||||||||||||
| self._f.write(self._buffer) | ||||||||||||||||||||
| self._buffer.clear() | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def finalise(self) -> None: | ||||||||||||||||||||
| if self._finalised: | ||||||||||||||||||||
| return | ||||||||||||||||||||
| self.flush() | ||||||||||||||||||||
| self._f.write(b"CRC2") | ||||||||||||||||||||
| self._f.write(struct.pack("<I", self._crc & 0xFFFFFFFF)) | ||||||||||||||||||||
| self._finalised = True | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def __enter__(self) -> "ChecksumWriterFast": | ||||||||||||||||||||
| return self | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def __exit__( | ||||||||||||||||||||
| self, | ||||||||||||||||||||
| exc_type, | ||||||||||||||||||||
| exc, | ||||||||||||||||||||
| tb, | ||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||
| if exc_type is None: | ||||||||||||||||||||
| self.finalise() | ||||||||||||||||||||
|
Comment on lines
+8
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Maintainability & Code Quality | π Major | β‘ Quick win The benchmark compares two identical buffered implementations and cannot validate the claimed 1.4Γ improvement. After this PR, The benchmark was meaningful before the PR applied the buffering to Consider removing this file, or replacing Also applies to: 66-67 π€ Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| class MockFile(io.BytesIO): | ||||||||||||||||||||
| def write(self, data): | ||||||||||||||||||||
| return super().write(data) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def run_test(cls): | ||||||||||||||||||||
| f = MockFile() | ||||||||||||||||||||
| start = time.time() | ||||||||||||||||||||
| with cls(f) as cw: | ||||||||||||||||||||
| for _ in range(100000): | ||||||||||||||||||||
| cw.write(b"hello ") | ||||||||||||||||||||
| cw.write(b"world!") | ||||||||||||||||||||
| end = time.time() | ||||||||||||||||||||
| return end - start | ||||||||||||||||||||
|
|
||||||||||||||||||||
| t1 = run_test(ChecksumWriter) | ||||||||||||||||||||
| t2 = run_test(ChecksumWriterFast) | ||||||||||||||||||||
| print(f"Old: {t1:.4f}s") | ||||||||||||||||||||
| print(f"New: {t2:.4f}s") | ||||||||||||||||||||
|
Comment on lines
+66
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Maintainability & Code Quality | π΅ Trivial | β‘ Quick win Benchmark executes at import time instead of using a test framework. Lines 66β69 run immediately when the module is imported, which prevents integration with pytest/unittest discovery and can cause unexpected side effects. Wrap the benchmark in a β»οΈ Proposed refactor- t1 = run_test(ChecksumWriter)
- t2 = run_test(ChecksumWriterFast)
- print(f"Old: {t1:.4f}s")
- print(f"New: {t2:.4f}s")
+ if __name__ == "__main__":
+ t1 = run_test(ChecksumWriter)
+ t2 = run_test(ChecksumWriterFast)
+ print(f"Old: {t1:.4f}s")
+ print(f"New: {t2:.4f}s")π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import struct | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import zlib | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from snapvec._file_format import ChecksumWriter | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MockFile(io.BytesIO): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def write(self, data): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return super().write(data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_writer(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f = MockFile() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| with ChecksumWriter(f) as cw: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cw.write(b"hello ") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cw.write(b"world!") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| f.seek(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| res = f.read() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print("result len:", len(res)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert res[:12] == b"hello world!" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π― Functional Correctness | π΅ Trivial | β‘ Quick win Test does not verify the CRC trailer β only the payload prefix is checked. The assertion β»οΈ Proposed additional assertions assert res[:12] == b"hello world!"
+
+ expected_crc = zlib.crc32(b"hello world!") & 0xFFFFFFFF
+ assert res[12:16] == _TRAILER_MAGIC
+ assert res[16:20] == struct.pack("<I", expected_crc)
+ assert len(res) == 20This requires importing π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test_writer() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print("Success") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π©Ί Stability & Availability | π‘ Minor | β‘ Quick win π§© Analysis chainπ Script executed: git ls-files | rg '(^|/)test_checksum\.py$|(^|/)pytest|(^|/)tests?/'Repository: stffns/snapvec Length of output: 557 π Script executed: fd -a 'test_checksum.py' . && echo '---' && if [ -f test_checksum.py ]; then cat -n test_checksum.py; fiRepository: stffns/snapvec Length of output: 790 π Script executed: if [ -f test_checksum.py ]; then cat -n test_checksum.py; else fd -a 'test_checksum.py' .; fiRepository: stffns/snapvec Length of output: 750 π Script executed: set -e
fd -a '^test_checksum\.py$' . || true
echo '---'
for f in $(fd -a '^test_checksum\.py$' .); do
echo "FILE: $f"
cat -n "$f"
done
echo '---'
git ls-files | rg '(^|/)(pytest\.ini|pyproject\.toml|tox\.ini|setup\.cfg|conftest\.py)$'Repository: stffns/snapvec Length of output: 885 Move the bare test call under a main guard
π€ Prompt for AI Agents
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Maintainability & Code Quality | π΅ Trivial | β‘ Quick win Test executes at import time instead of using a test framework.
β»οΈ Proposed refactor- test_writer()
- print("Success")
+ if __name__ == "__main__":
+ test_writer()
+ print("Success")π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Maintainability & Code Quality | π‘ Minor | β‘ Quick win
Add a blank line after the heading.
markdownlintreports MD022 because the heading is immediately followed by the paragraph.Proposed fix
## 2024-05-18 - Batch file writes with bytearray buffering + **Learning:** Batching multiple small file writesπ Committable suggestion
π§° Tools
πͺ markdownlint-cli2 (0.22.1)
[warning] 5-5: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
π€ Prompt for AI Agents
Source: Linters/SAST tools