Where Does the Performance Go? Understanding and Recovering Performance Regressions in C-to-Rust Translation
Organizations: Department of Computer Science, Fudan University, China · Nanyang Technological University, Singapore
Abstract
Rust is increasingly adopted as a memory-safe successor to C, and transpilers such as \crust{} can migrate entire C projects while preserving their behavior. Preserving behavior, however, does not guarantee preserving performance. We study this gap on 20 real-world C projects and their \crust{} translations. On the eight projects used for analysis, the translated Rust is more than 3% slower than the original C on 12 of 19 operations, by up to 62.2%. By comparing execution-cost profiles, optimized LLVM IR, and compiler optimization remarks across each C--Rust pair, we trace these slowdowns to nine recurring performance issues, spanning residual runtime work, missed compiler optimizations, and C-style source patterns, and derive an optimization direction for each. We then present \ourtool{}, an LLM-based approach that encodes these issues as reusable optimization rules to recover the performance of translated Rust. \ourtool{} profiles each project to locate hot functions and matches them against rule-specific patterns. The LLM then checks each rule's rewrite preconditions against the project context before rewriting, and a rewrite is kept only if it passes both functional and performance validation. On 41 operations from twelve held-out projects, \ourtool{} preserves C outputs on all tested workloads and speeds up 40 operations over the \crust{} translations, 18 of which end up faster than the original C. It outperforms the general-purpose coding agent \codex{} on 31 of the 37 operations where both preserve C outputs, and outperforms its own variant without rule guidance on 34 of the 41 operations.
Figures & tables
| Project | KLoC | Operations | Workload Inputs |
| libcsv | 1.0 | Parsing; Field escaping | 67 MiB CSV ‡ ; 10K escaped fields ‡ |
| libogg | 2.9 | Stream encoding; Decoding | 300K / 30K packets (1 KiB each) ‡ |
| xxHash | 3.9 | XXH64; XXH3; XXH32 hashing | 64 KiB buffer ‡ |
| libzahl | 5.7 | Multiplication; Long division; Modular exponentiation | 4,096-bit factors ‡ ; 8,192/4,096-bit dividend/divisor ‡ ; 4,096-bit base/modulus and 256-bit exponent ‡ |
| bzip2 | 5.9 | Compression; Decompression | Silesia files † ( 100 MiB); Derived .bz2 files ‡ ( 24 MiB) |
| binn | 6.4 | Serialization; Deserialization | 100K-element int32 list ‡ |
| Project | Operation ( ) | Project | Operation ( ) | Project | Operation ( ) |
| libcsv | Parsing | xxHash | XXH64 | binn | Serialization |
| Field escaping | XXH3 | Deserialization | |||
| libogg | Stream encoding | XXH32 | json-c | Parsing | |
| Decoding | libzahl | Multiplication | Serialization | ||
| bzip2 | Compression | Long division | heman | Distance-field | |
| Decompression | Modular exp. | Lighting |
| Cost | Project | Operation | Cost | Project | Operation | ||||
| Work | binn | Serialization | CPI | heman | Lighting | ||||
| Work | Deserialization | CPI | libcsv | Field escaping | |||||
| Work | bzip2 | Decompression | Both | libcsv | Parsing | ||||
| Work | heman | Terrain generation | Both | xxHash | XXH3 | ||||
| Work | libzahl | Modular exponentiation | Both | heman | Distance-field pipeline | ||||
| CPI | xxHash | XXH32 | Both | libzahl | Multiplication |
| Performance Issue | Definition | Observed Pattern | Optimization Direction |
| Residual Bounds Checks | Bounds checks remain even when established index-range invariants prove them unnecessary. | Loop invariants establish valid indices, but optimized Rust IR retains checks leading to panic_bounds_check that are absent from C IR. | Express valid iteration bounds through slices or iterators to expose index–length relationships and enable elimination of redundant checks. |
| Redundant Conversion Saturation | Saturating float-to-integer conversions remain even when range invariants prove them unnecessary. | Optimized Rust IR uses llvm.fptosi.sat or llvm.fptoui.sat for range-bounded operands, whereas C IR uses fptosi or fptoui . | Avoid unnecessary saturation with to_int_unchecked for finite operands whose values, truncated toward zero, fit the target integer type. |
| Missed Loop Vectorization | Loop vectorization achieved in C is lost after translation to Rust. | LLVM vectorizes a C loop but leaves its Rust counterpart scalar; remarks identify control-flow, call, or memory-dependence barriers. | Restructure blocking control flow or calls, or expose valid dependence information to enable loop vectorization. |
| Missed Hot-Callee Inlining | Hot calls inlined in C remain uninlined in translated Rust. | A hot call is inlined in C but retained in Rust; IR comparison reveals missing inlining attributes, or remarks report excessive cost. | Restore missing inlining annotations or simplify the callee to reduce its estimated inlining cost. |
| Repeated Invariant Dispatch | Loops dispatch on callbacks or mode values that remain invariant across iterations. | A loop repeatedly invokes an invariant callback, e.g., cb.expect(...)(args) , or branches on an invariant mode. | Resolve loop-invariant callback or mode dispatch once before the loop, then execute a specialized loop for the selected target or mode. |
| Inefficient Buffer Management | Repeated allocation or fixed-increment growth incurs avoidable heap-allocation overhead. | An internal buffer is repeatedly allocated and freed, or grows by fixed increments through realloc . | Reuse buffers to reduce allocations; reserve capacity upfront or use geometric growth to reduce reallocations. |
Code Translation
Code translation converts programs between languages, frameworks, and representations while aiming to preserve behavior and sometimes improve safety or performance.
See all 42 papers