15-rustTermsLevel_15Link-Time Optimization (LTO)

Link-Time Optimization (LTO)

Level 15 — Performance & Optimization An advanced LLVM compiler optimization pipeline configured in Cargo.toml (lto = true or lto = "thin") that defers optimization passes until the final link step, allowing cross-crate function inlining, global dead-code elimination, and whole-program binary size reduction.


1. Prerequisites

  • Crate — Separate compilation units in Rust Cargo builds.

2. Term Category

Rust Compiler Optimization (cross-codegen-unit LTO link optimizations): Link-Time Optimization (LTO) is a compilation technique provided by LLVM. Normally, rustc compiles each crate (and each codegen unit within a crate) independently into machine code object files (.o / .rlib), preventing optimizations from spanning across crate boundaries unless #[inline] is explicitly marked. LTO defers code generation optimization until the linker stage, giving the LLVM linker a whole-program view of all crates in the dependency graph.


3. Explanation

(1) Design Motivation — "Why did we design this?"

By default, Cargo prioritizes fast compilation times by splitting a Rust project into multiple independent compilation units:

  • Crate A (serde), Crate B (tokio), Crate C (reqwest), and your application crate are compiled separately into binary object files.
  • The standard linker simply stitches these pre-compiled object binary blocks together at the end.

This separate compilation model creates optimization walls at crate boundaries:

  1. Small helper functions in third-party dependency crates that lack explicit #[inline] attributes CANNOT be inlined into your application code.
  2. Unused code inside third-party dependency crates cannot be completely purged if the pre-compiled object file retains references to it.
  3. Cross-crate constant propagation and vectorization passes are blocked.

Link-Time Optimization (LTO) tears down these walls. Instead of generating machine code during initial crate compilation, rustc emits LLVM Bitcode (.bc). When the linker runs, LLVM analyzes the bitcode of every crate in the entire application at once:

  • Cross-Crate Inlining: Small functions from external crates are inlined into your app even without #[inline].
  • Global Dead Code Elimination: Unused functions across all dependencies are aggressively stripped out, shrinking WebAssembly (.wasm) and embedded binary sizes by up to 20–40%.
  • Whole-Program Vectorization: Loop optimization passes inspect data flows across crate boundaries.

(2) LTO Configuration Modes in Cargo.toml

Rust supports several LTO modes configured in [profile.release]:

ModeCargo.toml SettingDescription / Trade-off
Off (Default)lto = falseFast linking; cross-crate inlining limited to #[inline] functions; 16 codegen units.
Thin LTOlto = "thin"Recommended balance. Performs parallelized cross-crate optimization; fast compile times with 80-90% of Full LTO performance.
Fat / Full LTOlto = true or lto = "fat"Merges ALL bitcode into a single LLVM module. Maximum runtime performance & smallest binary size; very slow link time.
Disabledlto = "off"Completely disables LTO (even thin LTO across codegen units).

(3) Reality Metaphor

Imagine an Architectural Modular Prefab Construction Site:

  • Default Compilation (No LTO) is like buying pre-assembled room modules from 5 different factories (Crates A, B, C):
    • Factory A delivers a pre-painted bedroom; Factory B delivers a pre-plumbed bathroom.
    • The construction crew at the site (the standard linker) simply bolts the room boxes together.
    • The crew cannot merge shared walls or trim excess copper pipe hidden inside the pre-assembled Factory B box (cannot optimize across crate boundaries).
  • Link-Time Optimization (lto = true) is handing all 5 factory blueprints to a master architect before any room is built:
    • The architect analyzes the entire house layout at once (whole-program bitcode view).
    • The architect eliminates duplicate electrical panels, merges shared walls, and reroutes pipes seamlessly across room boundaries (cross-crate inlining & dead-code elimination).
    • It takes the architect longer to review the combined blueprints (longer build link time), but the resulting house is lighter, stronger, and significantly more efficient.

(4) Code Examples

Short Snippet (Configuring LTO in Cargo.toml)

# Cargo.toml

[package]
name = "high_perf_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0"
serde_json = "1.0"

# Configure Link-Time Optimization for release builds
[profile.release]
opt-level = 3        # Maximum optimizations
lto = "thin"         # Enable Thin LTO (Recommended for high performance + reasonable compile time)
codegen-units = 1    # Reduce codegen units to 1 for maximum LTO effectiveness
panic = "abort"      # Strip stack unwinding tables to shrink binary size further

Fuller Example (Demonstrating LTO Impact on Cross-Crate Optimization)

// Assume `external_math_crate` is a dependency crate containing a helper function WITHOUT `#[inline]`:
//
// external_math_crate/src/lib.rs:
// pub fn multiply_by_two(val: u64) -> u64 { val * 2 }

fn main() {
    let numbers: Vec<u64> = (0..1_000_000).collect();

    // Without LTO: Calling `multiply_by_two` requires a cross-crate `call` instruction on every loop.
    // With `lto = "thin"` or `lto = true`: LLVM inspects the bitcode of `external_math_crate`,
    // INLINES `multiply_by_two` directly into this loop, and SIMD-vectorizes the loop!
    let sum: u64 = numbers.iter().map(|&x| x * 2).sum();

    println!("Processed 1,000,000 items with cross-crate LTO optimization: {}", sum);
}

4. Common Mistakes & Pitfalls

Mistake 1: Enabling Fat LTO (lto = true) on Debug Profile ([profile.dev])

The mistake: Adding lto = true to [profile.dev] or setting lto = true during daily edit-compile-test development cycles.

Why it's wrong: Full LTO merges all crate bitcode into a single LLVM thread, increasing link time from 2 seconds to 2 minutes on large projects. This destroys developer iteration speed. LTO should be reserved for release profiles ([profile.release]) or CI production builds.

Incorrect:

# ❌ Anti-pattern: Destroys debug compile speed!
[profile.dev]
lto = true 

Fix:

# Correct: Enable LTO only for production release builds
[profile.release]
lto = "thin"

Mistake 2: Leaving codegen-units High while expecting Maximum Fat LTO

The mistake: Setting lto = true while leaving codegen-units = 16 in Cargo.toml.

Why it's wrong: codegen-units splits a crate into multiple parallel compilation chunks. Multiple codegen units restrict LLVM's ability to perform whole-crate inlining. For maximum Fat LTO efficiency and smallest binary size, set codegen-units = 1.

Incorrect:

[profile.release]
lto = true
codegen-units = 16 # ❌ Limits full LTO optimization effectiveness

Fix:

[profile.release]
lto = true
codegen-units = 1 # Maximum optimization & smallest binary size

Mistake 3: Comparing Benchmarks without Re-building in --release

The mistake: Editing Cargo.toml LTO settings and running cargo test or cargo run without the --release flag.

Why it's wrong: Cargo profiles are separate. Editing [profile.release] has ZERO effect on standard cargo run or cargo test builds. You must pass --release for [profile.release] LTO settings to take effect.


5. Practice Exercises

Exercise 1: High-Throughput Packet Processor with Thin LTO

Scenario: Problem Statement: You are building a high-throughput network packet processing pipeline for a microservice. Packet header verification, payload obfuscation, and checksum calculations are split across crate boundaries. External helper functions do NOT specify explicit #[inline] attributes. Configure Cargo for Thin LTO (lto = "thin"), implement a complete packet processor with payload masking and validation routines, and write unit tests with assertions proving packet transformation correctness.

Requirements:

Answer

Implementation

// Cargo.toml configuration required for this exercise:
// [profile.release]
// opt-level = 3
// lto = "thin"
// codegen-units = 8

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PacketHeader {
    pub magic: u16,
    pub sequence: u32,
    pub payload_len: u16,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawFrame {
    pub header: PacketHeader,
    pub payload: [u8; 8],
    pub checksum: u8,
}

/// Module simulating an external dependency crate without #[inline] annotations.
pub mod external_crypto_crate {
    /// Computes a XOR checksum over the payload.
    /// Note: LTO enables cross-crate inlining even without explicit #[inline]!
    pub fn compute_checksum(header_magic: u16, payload: &[u8]) -> u8 {
        let magic_bytes = header_magic.to_le_bytes();
        let mut acc = magic_bytes[0] ^ magic_bytes[1];
        for &byte in payload {
            acc = acc.wrapping_add(byte) ^ 0xA5;
        }
        acc
    }

    /// Applies a bitwise XOR mask to obfuscate payload data.
    pub fn apply_mask(payload: &mut [u8; 8], mask_key: u8) {
        for byte in payload.iter_mut() {
            *byte ^= mask_key;
        }
    }
}

pub struct PacketProcessor {
    pub accepted_count: u64,
    pub rejected_count: u64,
}

impl PacketProcessor {
    pub fn new() -> Self {
        Self {
            accepted_count: 0,
            rejected_count: 0,
        }
    }

    /// Validates, transforms, and processes an incoming network frame.
    pub fn process_frame(&mut self, mut frame: RawFrame, mask_key: u8) -> Option<RawFrame> {
        // Validate magic header value (0x4E50 = 'NP' for Network Packet)
        if frame.header.magic != 0x4E50 {
            self.rejected_count += 1;
            return None;
        }

        // Verify checksum using cross-crate helper (inlined via Thin LTO)
        let expected_checksum = external_crypto_crate::compute_checksum(
            frame.header.magic,
            &frame.payload,
        );

        if frame.checksum != expected_checksum {
            self.rejected_count += 1;
            return None;
        }

        // Apply payload masking (vectorized across crate boundaries via LTO)
        external_crypto_crate::apply_mask(&mut frame.payload, mask_key);
        
        // Re-calculate updated checksum post-masking
        frame.checksum = external_crypto_crate::compute_checksum(
            frame.header.magic,
            &frame.payload,
        );

        self.accepted_count += 1;
        Some(frame)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_packet_processing() {
        let mut processor = PacketProcessor::new();
        let payload = [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80];
        let magic = 0x4E50;
        let initial_checksum = external_crypto_crate::compute_checksum(magic, &payload);

        let frame = RawFrame {
            header: PacketHeader {
                magic,
                sequence: 101,
                payload_len: 8,
            },
            payload,
            checksum: initial_checksum,
        };

        let mask_key = 0xFF;
        let processed = processor.process_frame(frame, mask_key);

        assert!(processed.is_some(), "Valid frame should be processed successfully");
        let processed_frame = processed.unwrap();

        // Verify payload transformation (0x10 ^ 0xFF = 0xEF, etc.)
        let expected_payload = [0xEF, 0xDF, 0xCF, 0xBF, 0xAF, 0x9F, 0x8F, 0x7F];
        assert_eq!(processed_frame.payload, expected_payload);

        // Verify updated checksum matches newly masked payload
        let expected_new_checksum = external_crypto_crate::compute_checksum(magic, &expected_payload);
        assert_eq!(processed_frame.checksum, expected_new_checksum);

        assert_eq!(processor.accepted_count, 1);
        assert_eq!(processor.rejected_count, 0);
    }

    #[test]
    fn test_invalid_magic_header_rejection() {
        let mut processor = PacketProcessor::new();
        let frame = RawFrame {
            header: PacketHeader {
                magic: 0xDEAD, // Invalid magic identifier
                sequence: 102,
                payload_len: 8,
            },
            payload: [0; 8],
            checksum: 0,
        };

        let result = processor.process_frame(frame, 0x00);
        assert!(result.is_none());
        assert_eq!(processor.accepted_count, 0);
        assert_eq!(processor.rejected_count, 1);
    }

    #[test]
    fn test_checksum_mismatch_rejection() {
        let mut processor = PacketProcessor::new();
        let frame = RawFrame {
            header: PacketHeader {
                magic: 0x4E50,
                sequence: 103,
                payload_len: 8,
            },
            payload: [0x01; 8],
            checksum: 0xFF, // Corrupted checksum
        };

        let result = processor.process_frame(frame, 0xAA);
        assert!(result.is_none());
        assert_eq!(processor.accepted_count, 0);
        assert_eq!(processor.rejected_count, 1);
    }
}

Technical Explanation

  1. Cross-Crate Inlining without #[inline]: Without LTO, calling external_crypto_crate::compute_checksum requires generating dynamic cross-crate function call instructions. With lto = "thin", LLVM emits module summaries and performs cross-crate function inlining directly into process_frame.
  2. SIMD & Loop Unrolling: The apply_mask function operates on fixed 8-byte payload arrays. When inlined across crate boundaries via Thin LTO, LLVM detects the fixed iteration bounds and unrolls/vectorizes the operation into fast SIMD or 64-bit register operations.
  3. Thin LTO vs Fat LTO Balance: Thin LTO compiles codegen units in parallel, maintaining fast compile and link times while delivering 80-90% of the optimization benefits of Fat LTO.

Exercise 2: Embedded Telemetry Packer with Fat LTO Dead-Code Elimination

Scenario: Problem Statement: In a #![no_std] embedded environment (such as an ARM Cortex-M microcontroller), SRAM and Flash memory are severely constrained. You are building a sensor telemetry serializer. External dependency crates often contain diagnostic string formatting utilities that are unused in production. Configure Cargo for Fat LTO (lto = true, codegen-units = 1, opt-level = "z"), write a #![no_std] compatible telemetry frame serializer with bit-packing and CRC-8 validation, and include unit tests validating serialization accuracy.

Requirements:

Answer

Implementation

// Cargo.toml configuration for embedded binary size minimization:
// [profile.release]
// opt-level = "z"       # Optimize aggressively for binary size
// lto = true            # Enable Fat LTO for whole-program optimization
// codegen-units = 1     # Single compilation unit for maximum dead-code elimination
// panic = "abort"       # Strip stack unwinding tables

#![no_std]

/// Packed telemetry reading for embedded microcontrollers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SensorReading {
    pub sensor_id: u8,
    pub temperature_mC: i32, // Millidegrees Celsius
    pub humidity_ppm: u16,    // Parts per million
    pub status_flags: u8,    // Bitmask: [0: battery_low, 1: error_flag, 2: calibrated]
}

/// Simulated external library crate exporting diagnostic formatting and packing functions.
pub mod external_telemetry_lib {
    use super::SensorReading;

    /// Unused function in production binary!
    /// Without Fat LTO, this unused function would remain in linked object code.
    /// With Fat LTO, whole-program analysis purges this entire function (Dead-Code Elimination).
    pub fn format_verbose_json_debug(_reading: &SensorReading) -> &'static str {
        "{\"status\": \"unused_verbose_debug_string_that_wastes_flash_memory\"}"
    }

    /// Computes CRC-8 (polynomial 0x07) over byte slice.
    pub fn calculate_crc8(data: &[u8]) -> u8 {
        let mut crc: u8 = 0xFF;
        for &byte in data {
            crc ^= byte;
            for _ in 0..8 {
                if (crc & 0x80) != 0 {
                    crc = (crc << 1) ^ 0x07;
                } else {
                    crc <<= 1;
                }
            }
        }
        crc
    }

    /// Packs sensor reading into a fixed 8-byte payload buffer.
    pub fn pack_reading(reading: &SensorReading, out_buf: &mut [u8; 8]) {
        out_buf[0] = reading.sensor_id;
        
        let temp_bytes = reading.temperature_mC.to_le_bytes();
        out_buf[1] = temp_bytes[0];
        out_buf[2] = temp_bytes[1];
        out_buf[3] = temp_bytes[2];
        out_buf[4] = temp_bytes[3];

        let hum_bytes = reading.humidity_ppm.to_le_bytes();
        out_buf[5] = hum_bytes[0];
        out_buf[6] = hum_bytes[1];

        out_buf[7] = reading.status_flags;
    }
}

pub struct TelemetryPacker;

impl TelemetryPacker {
    /// Serializes reading into 9 bytes (8 data bytes + 1 CRC-8 byte).
    pub fn serialize(reading: &SensorReading) -> [u8; 9] {
        let mut buffer = [0u8; 9];
        let mut data_buf = [0u8; 8];
        
        external_telemetry_lib::pack_reading(reading, &mut data_buf);
        buffer[..8].copy_from_slice(&data_buf);
        
        let crc = external_telemetry_lib::calculate_crc8(&data_buf);
        buffer[8] = crc;
        
        buffer
    }

    /// Deserializes 9-byte packet and verifies CRC-8 checksum integrity.
    pub fn deserialize(buffer: &[u8; 9]) -> Result<SensorReading, &'static str> {
        let data_part = &buffer[..8];
        let received_crc = buffer[8];
        let calculated_crc = external_telemetry_lib::calculate_crc8(data_part);

        if received_crc != calculated_crc {
            return Err("CRC verification failed");
        }

        let sensor_id = buffer[0];
        let temperature_mC = i32::from_le_bytes([buffer[1], buffer[2], buffer[3], buffer[4]]);
        let humidity_ppm = u16::from_le_bytes([buffer[5], buffer[6]]);
        let status_flags = buffer[7];

        Ok(SensorReading {
            sensor_id,
            temperature_mC,
            humidity_ppm,
            status_flags,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_telemetry_serialization_roundtrip() {
        let reading = SensorReading {
            sensor_id: 0x42,
            temperature_mC: 23500,     // 23.5 °C
            humidity_ppm: 4500,        // 45.0% RH
            status_flags: 0b0000_0101, // Calibrated + Battery Low
        };

        let serialized = TelemetryPacker::serialize(&reading);
        assert_eq!(serialized[0], 0x42);
        
        // Verify CRC byte present at end of serialized array
        let expected_crc = external_telemetry_lib::calculate_crc8(&serialized[..8]);
        assert_eq!(serialized[8], expected_crc);

        // Deserialization roundtrip check
        let deserialized = TelemetryPacker::deserialize(&serialized).expect("Deserialization failed");
        assert_eq!(deserialized, reading);
    }

    #[test]
    fn test_crc_failure_detection() {
        let reading = SensorReading {
            sensor_id: 0x01,
            temperature_mC: 10000,
            humidity_ppm: 5000,
            status_flags: 0x00,
        };

        let mut serialized = TelemetryPacker::serialize(&reading);
        // Corrupt payload byte
        serialized[2] ^= 0xFF;

        let result = TelemetryPacker::deserialize(&serialized);
        assert_eq!(result, Err("CRC verification failed"));
    }
}

Technical Explanation

  1. Whole-Program Bitcode Analysis: When lto = true and codegen-units = 1 are configured, LLVM merges bitcode from all dependency crates into a single translation unit during linking.
  2. Global Dead-Code Elimination (DCE): Unreferenced functions, such as format_verbose_json_debug, along with associated static string literals and metadata, are purged completely from the final binary, preventing Flash memory bloat.
  3. #![no_std] and Panic Abort: Combining Fat LTO with panic = "abort" strips complex Rust stack unwinding tables (.eh_frame), generating ultra-compact firmware binaries ideal for microcontrollers and WebAssembly modules.

Exercise 3: Cross-Crate Trait Monomorphization & Devirtualization

Scenario: Problem Statement: In a real-time financial order matching engine, incoming orders are processed through fee tier strategies defined by trait implementations. In multi-crate software architectures, strategy execution across crate boundaries can introduce dispatch overhead. Configure Cargo for LTO to enable whole-program trait devirtualization, implement an order processing engine with traits and strategy implementations, and write unit tests using assert_eq! verifying fee calculations.

Requirements:

Answer

Implementation

// Cargo.toml configuration:
// [profile.release]
// opt-level = 3
// lto = "thin" // Enables cross-crate devirtualization and function inlining

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderType {
    Buy,
    Sell,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Order {
    pub id: u64,
    pub order_type: OrderType,
    pub price_cents: u64,
    pub quantity: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExecutionReport {
    pub order_id: u64,
    pub filled_quantity: u32,
    pub total_cost_cents: u64,
    pub fee_cents: u64,
}

/// Trait representing execution fee calculation strategy across crate boundaries.
pub trait FeeStrategy {
    fn calculate_fee(&self, total_cost_cents: u64) -> u64;
}

pub struct TieredFeeStrategy {
    pub vip_threshold_cents: u64,
    pub standard_fee_bps: u64, // Basis points (1 BPS = 0.01%)
    pub vip_fee_bps: u64,
}

impl FeeStrategy for TieredFeeStrategy {
    fn calculate_fee(&self, total_cost_cents: u64) -> u64 {
        let bps = if total_cost_cents >= self.vip_threshold_cents {
            self.vip_fee_bps
        } else {
            self.standard_fee_bps
        };
        (total_cost_cents * bps) / 10_000
    }
}

pub struct MatchingEngine<F: FeeStrategy> {
    pub fee_strategy: F,
    pub processed_volume_cents: u64,
}

impl<F: FeeStrategy> MatchingEngine<F> {
    pub fn new(fee_strategy: F) -> Self {
        Self {
            fee_strategy,
            processed_volume_cents: 0,
        }
    }

    /// Executes an order using monomorphized strategy calls optimized across crate boundaries.
    pub fn execute_order(&mut self, order: &Order) -> ExecutionReport {
        let total_cost_cents = (order.price_cents as u64) * (order.quantity as u64);
        let fee_cents = self.fee_strategy.calculate_fee(total_cost_cents);

        self.processed_volume_cents += total_cost_cents;

        ExecutionReport {
            order_id: order.id,
            filled_quantity: order.quantity,
            total_cost_cents,
            fee_cents,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_standard_tier_fee_execution() {
        let strategy = TieredFeeStrategy {
            vip_threshold_cents: 1_000_000, // $10,000
            standard_fee_bps: 20,           // 0.20%
            vip_fee_bps: 5,                 // 0.05%
        };

        let mut engine = MatchingEngine::new(strategy);

        let order = Order {
            id: 1001,
            order_type: OrderType::Buy,
            price_cents: 5000, // $50.00
            quantity: 100,     // Total: $5,000.00 (500,000 cents)
        };

        let report = engine.execute_order(&order);

        assert_eq!(report.order_id, 1001);
        assert_eq!(report.filled_quantity, 100);
        assert_eq!(report.total_cost_cents, 500_000);
        // Fee: 500,000 * 20 / 10,000 = 1,000 cents ($10.00)
        assert_eq!(report.fee_cents, 1_000);
        assert_eq!(engine.processed_volume_cents, 500_000);
    }

    #[test]
    fn test_vip_tier_fee_execution() {
        let strategy = TieredFeeStrategy {
            vip_threshold_cents: 1_000_000, // $10,000
            standard_fee_bps: 20,           // 0.20%
            vip_fee_bps: 5,                 // 0.05%
        };

        let mut engine = MatchingEngine::new(strategy);

        let order = Order {
            id: 1002,
            order_type: OrderType::Sell,
            price_cents: 20000, // $200.00
            quantity: 100,      // Total: $20,000.00 (2,000,000 cents)
        };

        let report = engine.execute_order(&order);

        assert_eq!(report.order_id, 1002);
        assert_eq!(report.total_cost_cents, 2_000_000);
        // VIP Fee: 2,000,000 * 5 / 10,000 = 1,000 cents ($10.00)
        assert_eq!(report.fee_cents, 1_000);
    }
}

Technical Explanation

  1. Monomorphization & Trait Devirtualization: Monomorphization creates specialized instances of generic structs and methods (MatchingEngine<TieredFeeStrategy>). When combined with LTO across crates, LLVM devirtualizes and inlines calculate_fee directly into execute_order.
  2. Branch Analysis & Constant Propagation: Inlining allows LLVM to evaluate branching logic (total_cost_cents >= vip_threshold_cents) in conjunction with caller context, optimizing conditional branches and register allocations across crate boundaries.
  3. Thin LTO Benefits for Large Systems: Thin LTO allows large, highly modular Rust projects to achieve direct-call execution speed without incurring the extreme compilation time penalties of Fat LTO.


7. Key Takeaways

  • Link-Time Optimization (LTO) defers LLVM optimization passes until the linking step, giving the compiler a whole-program view of all crates.
  • It enables cross-crate inlining (even without #[inline]), global dead-code elimination, and whole-program loop vectorization.
  • lto = "thin" is the recommended balance for release builds (fast parallel optimization with 80-90% of Fat LTO performance).
  • lto = true (Fat LTO) with codegen-units = 1 delivers maximum runtime speed and smallest binary size at the cost of longer link times.
  • Configure LTO exclusively inside [profile.release] in Cargo.toml.
Built with LogoFlowershow