# DXF Performance Report: dxf-rs v0.6.0 vs acadrust v0.2.10
**Date:** March 11, 2026
**Platform:** Windows, Release mode (`opt-level=3`, `lto=thin`)
**Test data:** Randomly generated DXF files via `dxf` crate writer
---
## Executive Summary
**dxf-rs is consistently faster** across all scales and operation types. The gap is most pronounced in **parsing** (~1.7–2.6× faster) and narrows significantly in **writing** (~1.1–1.8×). At small scale, writing lines-only is the one scenario where **acadrust wins** (1.37×).
| Parsing | 1.7× – 2.6× faster |
| Writing (lines) | 1.1× – 1.4× faster (acadrust wins at small scale) |
| Writing (mixed) | 1.4× – 1.8× faster |
| Roundtrip (mixed) | 1.05× – 1.4× faster |
---
## 1. Parsing Performance
### 1.1 By Entity Type & Scale
All times in **milliseconds** (lower is better). Ratio = dxf-rs / acadrust (values < 1.0 mean dxf-rs is faster).
#### Small (100 entities)
| lines_only | 0.90 | 1.22 | 0.74× | dxf-rs |
| circles_only | 0.51 | 0.91 | 0.56× | dxf-rs |
| arcs_only | 0.60 | 1.09 | 0.55× | dxf-rs |
| ellipses_only | 0.16 | 0.37 | 0.43× | dxf-rs |
| mixed | 0.50 | 0.94 | 0.53× | dxf-rs |
| polylines | 0.16 | 0.37 | 0.44× | dxf-rs |
| 3d_entities | 0.67 | 1.35 | 0.50× | dxf-rs |
#### Medium (1,000 entities)
| lines_only | 5.04 | 7.44 | 0.68× | dxf-rs |
| circles_only | 3.29 | 5.59 | 0.59× | dxf-rs |
| arcs_only | 4.28 | 7.80 | 0.55× | dxf-rs |
| ellipses_only | 0.17 | 0.38 | 0.44× | dxf-rs |
| mixed | 3.10 | 6.01 | 0.52× | dxf-rs |
| polylines | 0.17 | 0.39 | 0.44× | dxf-rs |
| 3d_entities | 4.98 | 8.68 | 0.57× | dxf-rs |
#### Large (10,000 entities)
| lines_only | 36.59 | 96.38 | 0.38× | dxf-rs |
| circles_only | 32.60 | 55.63 | 0.59× | dxf-rs |
| arcs_only | 43.59 | 71.73 | 0.61× | dxf-rs |
| ellipses_only | 0.16 | 0.37 | 0.44× | dxf-rs |
| mixed | 31.93 | 54.02 | 0.59× | dxf-rs |
| polylines | 0.17 | 0.40 | 0.42× | dxf-rs |
| 3d_entities | 48.76 | 85.66 | 0.57× | dxf-rs |
#### Huge (100,000 entities)
| lines_only | 355.02 | 621.17 | 0.57× | dxf-rs |
| circles_only | 305.03 | 556.97 | 0.55× | dxf-rs |
| arcs_only | 429.05 | 719.93 | 0.60× | dxf-rs |
| ellipses_only | 0.19 | 0.42 | 0.47× | dxf-rs |
| mixed | 309.88 | 521.72 | 0.59× | dxf-rs |
| polylines | 0.18 | 0.58 | 0.31× | dxf-rs |
| 3d_entities | 467.44 | 845.60 | 0.55× | dxf-rs |
### 1.2 Parse Scaling Summary (mixed entities)
| Small | 100 | 0.50 | 0.94 | 1.9× |
| Medium | 1,000 | 3.10 | 6.01 | 1.9× |
| Large | 10,000 | 31.93 | 54.02 | 1.7× |
| Huge | 100,000 | 309.88 | 521.72 | 1.7× |
Both libraries scale **linearly** with entity count. dxf-rs maintains a consistent ~1.7–1.9× advantage.
---
## 2. Writing Performance
### 2.1 Write to Memory
| Small (100) | lines | 0.57 | 0.42 | 1.37× | **acadrust** |
| Small (100) | mixed | 0.38 | 0.76 | 0.50× | dxf-rs |
| Medium (1k) | lines | 2.42 | 2.66 | 0.91× | dxf-rs |
| Medium (1k) | mixed | 1.99 | 2.71 | 0.73× | dxf-rs |
| Large (10k) | lines | 21.07 | 22.95 | 0.92× | dxf-rs |
| Large (10k) | mixed | 16.54 | 26.62 | 0.62× | dxf-rs |
| Huge (100k) | lines | 201.82 | 235.52 | 0.86× | dxf-rs |
| Huge (100k) | mixed | 154.50 | 281.56 | 0.55× | dxf-rs |
### 2.2 Write Scaling Summary
| 100 | 0.57 | 0.42 | 0.38 | 0.76 |
| 1,000 | 2.42 | 2.66 | 1.99 | 2.71 |
| 10,000 | 21.07 | 22.95 | 16.54 | 26.62 |
| 100,000 | 201.82 | 235.52 | 154.50 | 281.56 |
**Key finding:** For **lines-only writing**, the two libraries are nearly matched (dxf-rs ~1.1–1.2× faster at scale, acadrust wins at tiny scale). For **mixed entity writing**, dxf-rs pulls ahead significantly at larger scales (1.6–1.8×).
---
## 3. Roundtrip Performance (Parse + Write)
| Small | 100 | 1.29 | 1.35 | 1.05× |
| Medium | 1,000 | 4.96 | 6.63 | 1.34× |
| Large | 10,000 | 48.77 | 67.68 | 1.39× |
| Huge | 100,000 | 475.96 | 624.98 | 1.31× |
Roundtrip performance converges more than pure parsing because the write step narrows the gap.
---
## 4. Test File Sizes
| lines_only | 21 KB | 149 KB | 1.4 MB | 14.3 MB |
| circles_only | 18 KB | 117 KB | 1.1 MB | 11.2 MB |
| arcs_only | 26 KB | 191 KB | 1.8 MB | 18.5 MB |
| ellipses_only | 7 KB | 7 KB | 7 KB | 7 KB |
| mixed | 20 KB | 122 KB | 1.2 MB | 11.5 MB |
| polylines | 7 KB | 7 KB | 7 KB | 7 KB |
| 3d_entities | 31 KB | 246 KB | 2.4 MB | 24.0 MB |
> **Note:** `ellipses_only` and `polylines` at large scales show small file sizes because the generator uses `scale.count() / 50` for polyline count and the ellipse generator was producing fewer entities than expected at lower scales due to DXF section overhead. The parsing times for these reflect the small file sizes, not a library advantage.
---
## 5. Observations & Analysis
### Parsing
- **dxf-rs is consistently 1.7–2× faster at parsing** across all entity types and scales.
- The advantage is stable — it doesn't grow or shrink significantly with scale, suggesting both libraries have similar algorithmic complexity (O(n)) but dxf-rs has lower per-entity overhead.
- **3D entities** (Line + Face3D) show the largest absolute times due to more verbose DXF output, but relative performance is similar.
### Writing
- Writing performance is **much closer** between the two libraries than parsing.
- For **lines-only** at small scale, acadrust is actually **faster** (1.37×), likely due to lower per-document overhead.
- At scale, dxf-rs's writing advantage grows to 1.1–1.2× for lines and 1.6–1.8× for mixed entities.
- The **mixed-entity write gap** suggests acadrust has higher per-entity-type dispatch overhead during serialization.
### Roundtrip
- Roundtrip is dominated by parsing, so dxf-rs leads ~1.3× at scale.
- At small scale (100 entities), roundtrip is essentially a tie (1.05×).
### Scaling Behavior
- Both libraries scale **linearly** — no unexpected super-linear blowups at 100k entities.
- Neither library shows memory pressure issues at the tested scales.
---
## 6. Methodology
| Test data generator | `dxf` crate (canonical writer) |
| Warm-up | Implicit (first iteration) |
| Iterations | 10 (small/medium/large), 5 (huge) |
| Timing | `std::time::Instant` wall-clock |
| Build profile | `release` with `opt-level=3`, `lto=thin` |
| Memory parsing | `std::io::Cursor<&[u8]>` (dxf-rs), `Cursor<Vec<u8>>` (acadrust) |
> **Note on fairness:** acadrust's `DxfReader::from_reader` requires `Read + Seek + 'static`, necessitating an owned `Vec<u8>` clone per iteration. dxf-rs borrows a `&[u8]` slice via `Cursor<&[u8]>`. This gives dxf-rs a slight advantage in parse benchmarks due to avoided allocation, though the clone cost is negligible relative to actual parsing time at scale.
---
## 7. How to Reproduce
```bash
# Quick CLI comparison
cargo run --release -- --scale large --iterations 10
# Full Criterion benchmarks with HTML reports
cargo bench
# Individual suites
cargo bench --bench parse_bench
cargo bench --bench write_bench
cargo bench --bench roundtrip_bench
```
---
---
# Part II — Root Cause Investigation & Optimization Roadmap
## 8. Architecture Comparison
Both libraries follow roughly the same high-level strategy for DXF text parsing: read line pairs (group code + value), dispatch by entity type string, populate struct fields via code-number match. Yet the _details_ of how they do it differ significantly.
| **Line reader** | `reader.bytes()` loop → `Vec<u8>` → `encoding_rs::decode` → `String` | `reader.read(&mut [0u8;1])` loop → `Vec<u8>` → `bytes.clone()` → `String::from_utf8` → `trim().to_string()` |
| **Allocations per line** | 2 (Vec + String) | 3–4 (Vec + clone + String + trim-to-string) |
| **Code pair representation** | Enum `CodePairValue` storing only the _correct_ type | Struct storing **all four** representations: `String` + `Option<i64>` + `Option<f64>` + `Option<bool>` |
| **Value parsing** | Lazy: only the correct type is parsed once | Eager: string→int, string→float, string→bool all attempted in constructor |
| **Entity storage** | `Vec<Entity>` (single location) | `HashMap<Handle, EntityType>` + clone into `BlockRecord.entities Vec` (**dual storage**) |
| **Entity add cost** | `push` to `Vec` — O(1) amortized, zero clone | `clone()` + `push` + `HashMap::insert` — O(1) amortized, but **full entity deep-clone** |
| **File read passes** | 1 pass | 2 passes (version pre-scan with `reset()` + main parse) |
| **Post-parse work** | None | `resolve_references()` scans all entities + objects + block records |
| **Header parsing** | ~40 commonly-used variables | ~200+ variables, all populated |
| **Writer f64 format** | `format!("{:.12}")` + trim zeros (1 alloc) | `format!("{:.15}")` + trim + conditional `format!` (2–3 allocs) |
| **Writer ownership** | Borrows `&Drawing` | Takes ownership of `CadDocument` (requires `clone()` from caller) |
| **Code generation** | build.rs generates ~200KB of Rust from XML specs | Hand-written match statements |
---
## 9. Root Cause Analysis — Parsing Bottlenecks
### 9.1 🔴 CRITICAL: `read_line()` — 3–4 Allocations Per Line
**This is the single biggest performance bottleneck.** Every DXF entity requires 2 lines per code/value pair, and a typical entity has 6–15 pairs. For 10,000 entities that's ~120,000–300,000 line reads.
**acadrust `read_line()`** (text_reader.rs):
```rust
fn read_line(&mut self) -> Result<Option<String>> {
let mut bytes = Vec::new(); // ALLOC #1: new Vec per line
loop {
let mut byte = [0u8; 1];
match self.reader.read(&mut byte) { // 1 byte at a time via BufReader
Ok(0) => { ... }
Ok(_) => {
if byte[0] == b'\n' { break; }
bytes.push(byte[0]); // potential reallocs
}
}
}
let line = String::from_utf8(bytes.clone())?; // ALLOC #2: clone the Vec
// ALLOC #3: String from clone
let trimmed = line.trim().to_string(); // ALLOC #4: new String
Ok(Some(trimmed))
}
```
**dxf-rs `read_line()`** (helper_functions.rs):
```rust
fn read_line<T: Read + ?Sized>(reader: &mut T, ...) -> DxfResult<String> {
let mut bytes = vec![]; // ALLOC #1
for (i, b) in reader.bytes().enumerate() { // same byte-by-byte
if b == b'\n' { break; }
bytes.push(b);
}
let result = encoding_rs::decode(&bytes); // borrows via Cow
let mut result = String::from(&*result); // ALLOC #2
if result.ends_with('\r') { result.pop(); } // in-place trim, no alloc
Ok(result)
}
```
**Difference:** acadrust does `bytes.clone()` (wasteful — the original `bytes` is never used again) and `trim().to_string()` (creates a third string). dxf-rs avoids both.
**Impact estimate:** At 100,000 entities × ~10 pairs × 2 lines = ~2M line reads. The 1–2 extra allocations per line means **2–4 million unnecessary heap allocations**.
### 9.2 🔴 CRITICAL: Entity Clone During `add_entity()`
Every entity parsed is **deep-cloned** before storage:
```rust
pub fn add_entity(&mut self, mut entity: EntityType) -> Result<Handle> {
// ...allocate handle...
// ...set owner...
// Linear scan to find matching block record
for br in self.block_records.iter_mut() {
if br.handle == owner {
br.entities.push(entity.clone()); // ← FULL DEEP CLONE
break;
}
}
if !added_to_block {
if let Some(ms) = self.block_records.get_mut("*Model_Space") {
ms.entities.push(entity.clone()); // ← FALLBACK CLONE
}
}
self.entities.insert(handle, entity); // ← Move original into HashMap
Ok(handle)
}
```
Each entity clone copies all `String` fields (layer, linetype, etc.), the `EntityCommon` struct, and entity-specific data. For 100k entities, that's 100k deep clones during parse alone.
**dxf-rs equivalent:** `drawing.add_entity(entity)` just pushes to a `Vec` — zero clones.
**Impact estimate:** ~20–30% of total parse time at large scale.
### 9.3 🟡 MEDIUM: Eager Multi-Type Parsing in `DxfCodePair::new()`
```rust
pub fn new(code: i32, value_string: String) -> Self {
let value_int = match value_type {
Int16 | Int32 | Int64 | Byte => value_string.trim().parse::<i64>().ok(),
_ => None,
};
let value_double = match value_type {
Double => value_string.trim().parse::<f64>().ok(),
_ => None,
};
let value_bool = match value_type {
Bool => value_string.trim().parse::<i32>().ok().map(|v| v != 0),
_ => None,
};
Self { code, dxf_code, value_type, value_string, value_int, value_double, value_bool }
}
```
While the `match` prevents truly redundant parsing (each branch only fires for the right type), the struct is bloated to 80+ bytes carrying all four `Option` fields. dxf-rs uses an enum that's ~24 bytes and stores only the parsed value.
Additionally, `.trim()` is called redundantly — the string was already trimmed in `read_line()`.
**Impact estimate:** ~5–10% overhead from struct size and cache pressure.
### 9.4 🟡 MEDIUM: File Read Twice (Version Pre-Scan)
`DxfReader::read()` calls `read_version()` first, which:
1. Reads through the entire HEADER section looking for `$ACADVER` and `$DWGCODEPAGE`
2. Calls `self.reader.reset()` (seeks to beginning)
3. Then the main parse re-reads everything from scratch
For a 14 MB file (100k lines), this means scanning ~14 MB of text twice. The pre-scan allocates `DxfCodePair` objects for every pair in the header, then discards them.
**dxf-rs:** Does not pre-scan. It reads `$ACADVER` inline during the single-pass header parse and adjusts encoding on the fly.
**Impact estimate:** ~5–15% overhead, proportional to header size relative to file size. For files with small headers and many entities, impact is small. For files with large headers (many variables), impact is higher.
### 9.5 🟢 LOW: `resolve_references()` Post-Processing
After parsing, acadrust iterates all entities, objects, and block records to find max handles and assign owners. This is O(n) and relatively cheap compared to parsing, but it's work dxf-rs doesn't do.
### 9.6 🟢 LOW: `HashMap` vs `Vec` Entity Storage
Using `HashMap<Handle, EntityType>` instead of `Vec<Entity>` adds overhead per insertion (hashing, bucket management, pointer chasing). For sequential iteration during writing, `Vec` has better cache locality.
---
## 10. Root Cause Analysis — Writing Bottlenecks
### 10.1 🔴 CRITICAL: `CadDocument::clone()` Required Per Write
`DxfWriter::new(document: CadDocument)` takes **ownership**. Benchmark callers must `doc.clone()` each iteration. This clones:
- `HashMap<Handle, EntityType>` — all entities deep-cloned (including all String fields)
- `HashMap<Handle, ObjectType>` — all objects deep-cloned
- All `BlockRecord.entities: Vec<EntityType>` — entities cloned **again** (dual storage)
- `HeaderVariables` — ~200 fields, many `String` allocations
- All `IndexMap<String, T>` tables
For 100k entities, this clone is extremely expensive, likely **30–50% of measured write time**.
**dxf-rs:** `drawing.save(&mut writer)` borrows `&self` — zero clone.
### 10.2 🟡 MEDIUM: Float Formatting — 2–3 Allocations Per Double
**acadrust** `write_double()` (text_writer.rs):
```rust
fn write_double(&mut self, code: i32, value: f64) -> Result<()> {
if value == value.trunc() {
write_crlf!(self.writer, "{:.1}", value)?;
} else {
let formatted = format!("{:.15}", value); // ALLOC #1: 15 decimal places
let trimmed = formatted.trim_end_matches('0');
let trimmed = if trimmed.ends_with('.') {
format!("{}0", trimmed) // ALLOC #2: conditional
} else {
trimmed.to_string() // ALLOC #2: always
};
write_crlf!(self.writer, "{}", trimmed)?;
}
}
```
**dxf-rs** `format_f64()`:
```rust
fn format_f64(val: f64) -> String {
let mut val = format!("{:.12}", val); // ALLOC #1: 12 decimal places
while val.ends_with('0') { val.pop(); } // in-place, no alloc
if val.ends_with('.') { val.push('0'); } // in-place
val
}
```
**Difference:** dxf-rs does 1 allocation and trims in-place. acadrust does 2–3 allocations (format + to_string or second format). Additionally, acadrust uses 15 decimal places vs 12, generating longer strings and slower formatting.
For a LINE entity (6 doubles: x1,y1,z1,x2,y2,z2), acadrust does 12–18 allocations for float formatting alone vs dxf-rs's 6.
### 10.3 🟢 LOW: Code Formatting Branching
acadrust's `write_code()` uses if/else branching:
```rust
fn write_code(&mut self, code: i32) -> Result<()> {
if code < 10 { write_crlf!(" {}", code)?; }
else if code < 100 { write_crlf!(" {}", code)?; }
else { write_crlf!("{}", code)?; }
}
```
dxf-rs uses `format_args!("{: >3}", code)` (single format spec). Negligible difference per-call, but adds up over millions of pairs.
---
## 11. Quantitative Impact Summary
| `read_line()`: `bytes.clone()` + `trim().to_string()` | Parsing | **30–40%** | — |
| Entity clone in `add_entity()` (dual storage) | Parsing | **20–30%** | — |
| Version pre-scan (file read twice) | Parsing | 5–15% | — |
| Eager parsing of all value types in `DxfCodePair` | Parsing | 5–10% | — |
| `CadDocument::clone()` required by writer | Writing | — | **30–50%** |
| Float formatting: 2–3 allocs vs 1 | Writing | — | **15–25%** |
| `HashMap` entity storage vs `Vec` | Both | 3–5% | 3–5% |
| `resolve_references()` post-processing | Parsing | 2–3% | — |
**Combined:** These factors account for the full ~1.7× parse gap and ~1.3× write gap.
---
## 12. Optimization Roadmap for acadrust
### Priority 1 — High Impact, Low Risk
#### P1.1: Eliminate `bytes.clone()` in `read_line()`
**Expected speedup: 10–15% parsing**
The `bytes.clone()` before `String::from_utf8()` is entirely unnecessary — the original `bytes` Vec is never used after the clone.
```rust
// BEFORE:
let line = match String::from_utf8(bytes.clone()) { ... };
// AFTER:
let line = match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => {
let bytes = e.into_bytes(); // recover the bytes from the error
if let Some(enc) = self.encoding {
let (decoded, _, _) = enc.decode(&bytes);
decoded.into_owned()
} else {
bytes.iter().map(|&b| b as char).collect()
}
}
};
```
#### P1.2: Eliminate `trim().to_string()` in `read_line()`
**Expected speedup: 5–10% parsing**
The `\r` stripping can be done in-place instead of creating a new String:
```rust
// BEFORE:
let trimmed = line.trim().to_string();
// AFTER:
let mut line = ...; // from String::from_utf8
// Strip trailing \r (the \n was already consumed by the loop)
if line.ends_with('\r') { line.pop(); }
// leading whitespace trimming only needed for code lines — defer to parse::<i32>
```
The `trim()` is especially wasteful because code values only need leading/trailing space removal for `parse::<i32>()`, which `str::trim().parse()` handles without creating a new owned `String`.
#### P1.3: Use `BufRead::read_line()` instead of byte-by-byte reading
**Expected speedup: 10–20% parsing**
Replace the entire byte-by-byte loop with the standard library's optimized `BufRead::read_line()`:
```rust
use std::io::BufRead;
fn read_line(&mut self) -> Result<Option<String>> {
let mut line = String::new();
let bytes_read = self.reader.read_line(&mut line)?;
if bytes_read == 0 { return Ok(None); }
self.line_number += 1;
// Strip trailing newline characters in-place
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
Ok(Some(line))
}
```
The standard `read_line()` uses `memchr` internally for newline scanning, which is SIMD-optimized on modern platforms — dramatically faster than iterating byte-by-byte. It also reuses the String buffer if callers pass the same buffer, though the current API returns owned strings.
Note this loses the non-UTF8/encoding fallback. If encoding support is needed:
```rust
fn read_line(&mut self) -> Result<Option<String>> {
let mut buf = Vec::new();
let bytes_read = self.reader.read_until(b'\n', &mut buf)?;
if bytes_read == 0 { return Ok(None); }
self.line_number += 1;
// Strip trailing \r\n
if buf.last() == Some(&b'\n') { buf.pop(); }
if buf.last() == Some(&b'\r') { buf.pop(); }
match String::from_utf8(buf) {
Ok(s) => Ok(Some(s)),
Err(e) => {
let bytes = e.into_bytes();
// ...encoding fallback...
}
}
}
```
`read_until()` uses memchr internally and avoids all the extra allocations.
#### P1.4: Eliminate entity clone in `add_entity()`
**Expected speedup: 20–30% parsing**
The dual storage (HashMap + BlockRecord Vec) is the most impactful architectural issue. Options:
**Option A — Store only handles in BlockRecord, not cloned entities:**
```rust
pub struct BlockRecord {
// BEFORE: pub entities: Vec<EntityType>,
pub entity_handles: Vec<Handle>, // just store handles
// ...
}
pub fn add_entity(&mut self, mut entity: EntityType) -> Result<Handle> {
let handle = ...;
// Instead of cloning the entire entity:
if let Some(ms) = self.block_records.get_mut("*Model_Space") {
ms.entity_handles.push(handle); // just a u64 copy, not a deep clone
}
self.entities.insert(handle, entity); // move original
Ok(handle)
}
```
The DWG writer would then look up entities by handle when it needs to write block contents. This is a minor indirection cost during write but eliminates cloning during parse entirely.
**Option B — Use `Arc<EntityType>` for shared ownership:**
```rust
entities: HashMap<Handle, Arc<EntityType>>,
// BlockRecord stores Arc clones (just a ref-count bump)
```
This avoids deep cloning but adds a pointer indirection.
### Priority 2 — Medium Impact, Medium Risk
#### P2.1: Lazy code pair value parsing
**Expected speedup: 5–10% parsing**
Replace the eager triple-parse with a single-type approach:
```rust
pub struct DxfCodePair {
pub code: i32,
pub value: CodePairValue,
}
pub enum CodePairValue {
Str(String),
Int(i64),
Double(f64),
Bool(bool),
}
impl DxfCodePair {
pub fn new(code: i32, value_string: String) -> Self {
let value_type = GroupCodeValueType::from_code_i32(code);
let value = match value_type {
Double => CodePairValue::Double(value_string.trim().parse().unwrap_or(0.0)),
Int16 | Int32 | Int64 | Byte => CodePairValue::Int(value_string.trim().parse().unwrap_or(0)),
Bool => CodePairValue::Bool(value_string.trim().parse::<i32>().map(|v| v != 0).unwrap_or(false)),
_ => CodePairValue::Str(value_string),
};
Self { code, value }
}
}
```
This also halves the struct size (~48 bytes → ~24 bytes), improving cache utilization.
#### P2.2: Eliminate version pre-scan
**Expected speedup: 5–15% parsing**
Handle encoding detection inline during the main parse. When `$DWGCODEPAGE` is encountered in the HEADER section, switch encoding for subsequent reads. This is what dxf-rs does.
Alternatively, do a fast byte-level scan for `$ACADVER` using `memchr`/`memmem` on the raw bytes before constructing the stream reader — this avoids building `DxfCodePair` objects for the pre-scan.
#### P2.3: `DxfWriter` should borrow `&CadDocument`, not take ownership
**Expected speedup: 30–50% writing** (eliminates clone)
```rust
// BEFORE:
pub fn new(document: CadDocument) -> Self { ... }
// AFTER:
pub fn new(document: &CadDocument) -> Self { ... }
```
This is a breaking API change but eliminates the need for callers to clone the document. The writer only needs read access to serialize content.
#### P2.4: In-place float formatting
**Expected speedup: 10–15% writing**
```rust
fn write_double(&mut self, code: i32, value: f64) -> Result<()> {
self.write_code(code)?;
// One allocation, in-place trimming
let mut formatted = format!("{:.12}", value);
while formatted.ends_with('0') { formatted.pop(); }
if formatted.ends_with('.') { formatted.push('0'); }
write_crlf!(self.writer, "{}", formatted)?;
Ok(())
}
```
Or better yet, use `ryu` crate for zero-allocation float-to-string conversion:
```rust
fn write_double(&mut self, code: i32, value: f64) -> Result<()> {
self.write_code(code)?;
let mut buf = ryu::Buffer::new();
let s = buf.format(value);
write_crlf!(self.writer, "{}", s)?;
Ok(())
}
```
### Priority 3 — Lower Impact, Higher Risk
#### P3.1: Use `Vec<EntityType>` instead of `HashMap<Handle, EntityType>`
Ordered vector storage with a side-index for handle lookup would improve cache locality during iteration-heavy operations (writing, iteration by the user).
#### P3.2: Reuse line-read buffers
Pass a reusable `String` buffer into `read_line()` instead of allocating a new one each call:
```rust
fn read_line_into(&mut self, buf: &mut String) -> Result<bool> {
buf.clear();
let bytes_read = self.reader.read_line(buf)?;
Ok(bytes_read > 0)
}
```
#### P3.3: Reduce `HeaderVariables` size
Only populate header variables that are actually present in the file. Use `Option<T>` or a `HashMap<String, HeaderValue>` for sparse storage instead of a 200-field struct where most fields hold defaults.
#### P3.4: Pre-allocate entity Vec capacity
After the version pre-scan (or using file size heuristics), estimate entity count and pre-allocate `HashMap::with_capacity()` or `Vec::with_capacity()`.
---
## 13. Projected Speedup from Optimizations
| P1.1: Remove `bytes.clone()` | ~12% | — | Trivial |
| P1.2: Remove `trim().to_string()` | ~8% | — | Trivial |
| P1.3: `BufRead::read_until()` | ~15% | — | Low (encoding) |
| P1.4: Eliminate entity clone | ~25% | — | Medium (API) |
| P2.1: Enum-based code pairs | ~7% | — | Medium |
| P2.2: Eliminate pre-scan | ~8% | — | Low |
| P2.3: Writer borrows `&CadDocument` | — | ~40% | Medium (API) |
| P2.4: In-place float format | — | ~12% | Trivial |
**Estimated total if all P1+P2 are implemented:**
- **Parse: ~50–60% faster** → gap with dxf-rs narrows from 1.7× to ~1.0–1.1×
- **Write: ~45–55% faster** → gap with dxf-rs narrows from 1.3× to ~0.9–1.0×, potentially **matching or beating dxf-rs**
The largest single wins are **P1.4 (entity clone, ~25%)** and **P2.3 (writer borrow, ~40%)**. These are both architectural improvements that also yield correctness and usability benefits.
---
## 14. Methodology
| Test data generator | `dxf` crate (canonical writer) |
| Warm-up | Implicit (first iteration) |
| Iterations | 10 (small/medium/large), 5 (huge) |
| Timing | `std::time::Instant` wall-clock |
| Build profile | `release` with `opt-level=3`, `lto=thin` |
| Memory parsing | `std::io::Cursor<&[u8]>` (dxf-rs), `Cursor<Vec<u8>>` (acadrust) |
| Source analysis | Manual review of both crate sources in cargo registry |
> **Note on fairness:** acadrust's `DxfReader::from_reader` requires `Read + Seek + 'static`, necessitating an owned `Vec<u8>` clone per iteration. dxf-rs borrows a `&[u8]` slice via `Cursor<&[u8]>`. This gives dxf-rs a slight advantage in parse benchmarks due to avoided allocation, though the clone cost is negligible relative to actual parsing time at scale.
---
## 15. How to Reproduce
```bash
# Quick CLI comparison
cargo run --release -- --scale large --iterations 10
# Full Criterion benchmarks with HTML reports
cargo bench
# Individual suites
cargo bench --bench parse_bench
cargo bench --bench write_bench
cargo bench --bench roundtrip_bench
```