---
title: "Optimisation au niveau des instructions : #[inline(always)]"
description: "Application stratégique de l'attribut #[inline(always)] de Rust pour l'optimisation au niveau des instructions, couvrant les modèles d'utilisation efficaces et les risques de surutilisation"
url: "https://mayorana.ch/fr/blog/instruction-level-optimization-inline-always"
language: "fr"
date: "2025-11-21"
author: "Mohamed Bennekrouf"
tags:
  - "rust"
  - "optimization"
  - "advanced"
en_version: "https://mayorana.ch/en/blog/instruction-level-optimization-inline-always.md"
---

# Optimisation au niveau des instructions : #[inline(always)]

L'attribut `#[inline(always)]` de Rust force le compilateur à intégrer le corps d'une fonction à chaque site d'appel, optimisant les performances au niveau des instructions en éliminant la surcharge des appels et en exposant davantage d'opportunités d'optimisation. Je l'utiliserais stratégiquement dans du code critique pour les performances, mais la surutilisation comporte des risques pour la taille du code, le temps de compilation et même l'efficacité à l'exécution. Voici comment je l'aborderais.

<div class="svg-container" style="margin:2rem 0;">
<svg class="lo8-fig" viewBox="0 0 800 230" width="100%" style="height:auto;max-width:780px;display:block;margin:0 auto;" role="img" aria-label="Un appel normal saute vers une fonction séparée et revient, tandis que inline(always) copie le corps de la fonction directement dans la boucle chaude">
<!-- style -->
<style>
.lo8-fig{--bg:#f8fafc;--box:#ffffff;--tx:#1e293b;--mut:#64748b;--ln:#cbd5e1;--ac:#FF6B00}
:root.dark .lo8-fig,[data-theme="dark"] .lo8-fig{--bg:#0f172a;--box:#1e293b;--tx:#f8fafc;--mut:#94a3b8;--ln:#475569}
.lo8-fig .box{fill:var(--box);stroke:var(--ln);stroke-width:1.5}
.lo8-fig .boxac{fill:var(--box);stroke:var(--ac);stroke-width:2}
.lo8-fig .ti{fill:var(--tx);font:700 14px ui-sans-serif,system-ui,sans-serif}
.lo8-fig .tx{fill:var(--tx);font:600 12px ui-sans-serif,system-ui,sans-serif}
.lo8-fig .mut{fill:var(--mut);font:11px ui-sans-serif,system-ui,sans-serif}
.lo8-fig .ln{stroke:var(--ln);stroke-width:1.5;fill:none}
</style>
<!-- defs -->
<defs>
<marker id="lo8arrowfr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="var(--ln)"/></marker>
</defs>
<!-- top: normal call -->
<text x="40" y="30" class="ti">Appel normal, dans une boucle</text>
<rect x="40" y="42" width="200" height="46" rx="6" class="box"/>
<text x="140" y="70" text-anchor="middle" class="tx">boucle parse_stream</text>
<path d="M240,65 L268,65" class="ln" marker-end="url(#lo8arrowfr)"/>
<rect x="270" y="42" width="180" height="46" rx="6" class="box"/>
<text x="360" y="70" text-anchor="middle" class="tx">saut + retour</text>
<path d="M450,65 L478,65" class="ln" marker-end="url(#lo8arrowfr)"/>
<rect x="480" y="42" width="220" height="46" rx="6" class="box"/>
<text x="590" y="70" text-anchor="middle" class="tx">corps de extract_bits</text>
<text x="620" y="105" class="mut">~5–10 cycles de surcharge par appel</text>
<!-- bottom: inline(always) -->
<text x="40" y="150" class="ti">#[inline(always)] extract_bits</text>
<rect x="40" y="162" width="200" height="46" rx="6" class="box"/>
<text x="140" y="190" text-anchor="middle" class="tx">boucle parse_stream</text>
<path d="M240,185 L268,185" class="ln" marker-end="url(#lo8arrowfr)"/>
<rect x="270" y="162" width="220" height="46" rx="6" class="boxac"/>
<text x="380" y="184" text-anchor="middle" class="tx">shr + and</text>
<text x="380" y="200" text-anchor="middle" class="mut">corps copié en ligne</text>
<text x="620" y="190" class="mut">pas de saut — mais dupliqué à chaque site d'appel</text>
</svg>
</div>

## Application stratégique

J'appliquerais `#[inline(always)]` dans des scénarios où :

- **Fonctions petites et fréquentes** : Une fonction minuscule appelée dans une boucle serrée, où la surcharge d'appel (configuration de la pile, sauts) est significative par rapport à son travail.
- **Opportunités d'optimisation** : L'inlining permet au compilateur de fusionner la fonction avec son appelant, simplifiant les branches ou les constantes.

### Exemple : Un utilitaire de manipulation de bits dans un parseur temps réel :

```rust
#[inline(always)]
fn extract_bits(value: u32, shift: u32, mask: u32) -> u32 {
    (value >> shift) & mask
}

fn parse_stream(data: &[u32]) -> u32 {
    let mut result = 0;
    for &val in data {
        result += extract_bits(val, 8, 0xFF); // Boucle chaude
    }
    result
}
```

**Pourquoi `#[inline(always)]` ?** : Sans inlining, chaque appel entraîne un saut et un retour (5-10 cycles sur x86_64). L'inlining réduit cela à un simple `shr` et `and`, et LLVM peut optimiser davantage la boucle (par exemple, la dérouler ou la vectoriser).

**Efficacité** : La simplicité de la fonction garantit que l'inlining réduit la surcharge, et la propagation de constantes (si shift et mask sont fixes) peut éliminer les opérations redondantes.

### Considérations :

- **Taille** : `extract_bits` est petite (2-3 instructions), donc l'inlining ne gonfle pas beaucoup.
- **Fréquence** : Utilisée dans une boucle chaude, justifiant la force.
- **Profiler d'abord** : Je confirmerais avec perf que la surcharge d'appel est un goulot d'étranglement avant de forcer l'inlining.

## Inconvénients de la surutilisation

### Augmentation de la taille du code
- L'inlining duplique le corps de la fonction partout où elle est appelée. Pour une fonction plus grande (par exemple, 20 instructions) appelée 100 fois, le binaire grossit de 2 000 instructions, gonflant le cache d'instructions (I-cache).
- **Impact** : Plus de défauts de I-cache, ralentissant l'exécution malgré moins d'appels.

### Temps de compilation
- LLVM doit optimiser chaque instance intégrée, augmentant le temps de compilation. Pour une base de code importante avec de nombreuses annotations `#[inline(always)]`, les builds pourraient ralentir de quelques secondes à plusieurs minutes.
- **Impact** : Itération plus lente, frustrante pour le développement.

### Risques de performances à l'exécution
- L'inlining excessif de grandes fonctions peut perturber la localité du I-cache, annulant les économies d'appel. Par exemple, intégrer une fonction de 50 instructions dans une boucle peut évincer d'autres codes chauds.
- Les heuristiques du compilateur (par exemple, avec `#[inline]` simple) équilibrent souvent cela mieux que l'inlining forcé.

<div class="svg-container" style="margin:2rem 0;">
<svg class="lo8b-fig" viewBox="0 0 800 295" width="100%" style="height:auto;max-width:780px;display:block;margin:0 auto;" role="img" aria-label="Vue côte à côte du cache d'instructions L1 : un petit helper inliné laisse de la place au reste du code chaud, alors qu'une grande fonction dupliquée à chaque point d'appel remplit le cache et l'évince">
<style>
.lo8b-fig{--bg:#f8fafc;--box:#ffffff;--tx:#1e293b;--mut:#64748b;--ln:#cbd5e1;--ac:#FF6B00}
:root.dark .lo8b-fig,[data-theme="dark"] .lo8b-fig{--bg:#0f172a;--box:#1e293b;--tx:#f8fafc;--mut:#94a3b8;--ln:#475569}
.lo8b-fig .frame{fill:none;stroke:var(--mut);stroke-width:2}
.lo8b-fig .blk{fill:var(--box);stroke:var(--ln);stroke-width:1.5}
.lo8b-fig .blkac{fill:var(--box);stroke:var(--ac);stroke-width:2.5}
.lo8b-fig .free{fill:none;stroke:var(--ln);stroke-width:1.5;stroke-dasharray:4 4}
.lo8b-fig .gone{fill:none;stroke:var(--ac);stroke-width:2;stroke-dasharray:4 4}
.lo8b-fig .box{fill:var(--box);stroke:var(--ln);stroke-width:1.5}
.lo8b-fig .ti{fill:var(--tx);font:700 13px ui-sans-serif,system-ui,sans-serif;text-anchor:middle}
.lo8b-fig .hd{fill:var(--mut);font:700 12px ui-sans-serif,system-ui,sans-serif;text-anchor:middle}
.lo8b-fig .tx{fill:var(--tx);font:600 12px ui-sans-serif,system-ui,sans-serif;text-anchor:middle}
.lo8b-fig .mut{fill:var(--mut);font:500 11px ui-sans-serif,system-ui,sans-serif;text-anchor:middle}
.lo8b-fig .ac{fill:var(--ac);font:700 12px ui-sans-serif,system-ui,sans-serif;text-anchor:middle}
</style>
<!-- panneau gauche : inlining mesuré -->
<text x="205" y="26" class="ti">Petit helper, inliné de force</text>
<rect x="30" y="40" width="350" height="170" rx="6" class="frame"/>
<text x="205" y="60" class="hd">Cache d'instructions L1 — 32 Ko</text>
<rect x="50" y="70" width="310" height="30" rx="4" class="blk"/>
<text x="205" y="90" class="tx">boucle parse_stream</text>
<rect x="50" y="104" width="310" height="30" rx="4" class="blkac"/>
<text x="205" y="124" class="ac">extract_bits — 2 instructions inlinées</text>
<rect x="50" y="138" width="310" height="30" rx="4" class="blk"/>
<text x="205" y="158" class="tx">autre code chaud — toujours résident</text>
<rect x="50" y="172" width="310" height="32" rx="4" class="free"/>
<text x="205" y="192" class="mut">marge disponible</text>
<!-- panneau droit : inlining excessif -->
<text x="595" y="26" class="ti">Fn de 50 instructions, 100 appels</text>
<rect x="420" y="40" width="350" height="170" rx="6" class="frame"/>
<text x="595" y="60" class="hd">Cache d'instructions L1 — 32 Ko</text>
<rect x="440" y="70" width="310" height="24" rx="4" class="blk"/>
<text x="595" y="87" class="tx">copie inlinée n° 1</text>
<rect x="440" y="98" width="310" height="24" rx="4" class="blk"/>
<text x="595" y="115" class="tx">copie inlinée n° 2</text>
<rect x="440" y="126" width="310" height="24" rx="4" class="blk"/>
<text x="595" y="143" class="tx">copie inlinée n° 3</text>
<rect x="440" y="154" width="310" height="24" rx="4" class="blk"/>
<text x="595" y="171" class="tx">copies n° 4 … n° 100</text>
<rect x="440" y="182" width="310" height="24" rx="4" class="gone"/>
<text x="595" y="199" class="ac">autre code chaud — évincé</text>
<!-- légende -->
<rect x="90" y="228" width="620" height="60" rx="6" class="box"/>
<text x="400" y="252" class="tx">La taille du cache est fixe — chaque copie prend les lignes de quelqu'un d'autre</text>
<text x="400" y="272" class="mut">Surveille les deux : size target/release/app pour .text, perf stat -e iTLB-load-misses pour le coût</text>
</svg>
</div>

## Stratégies d'atténuation

### Utilisation sélective
- Réserve `#[inline(always)]` pour les fonctions minuscules et fréquemment appelées dans les chemins chauds. Utilise `#[inline]` (une suggestion) pour les plus grandes, en faisant confiance au jugement de LLVM.
- **Exemple** : N'intègre pas un parseur complexe, mais intègre un accesseur de 2 lignes.

### Profilage
- Utilise `perf stat -e instructions,cycles` ou `cargo flamegraph` pour identifier la surcharge d'appel. Applique `#[inline(always)]` uniquement là où les données montrent un gain (par exemple, réduction de 10 %+ des cycles).
- Après optimisation, vérifie les défauts de I-cache (`perf stat -e iTLB-load-misses`) pour garantir l'absence de régression.

### Mesurer la taille du code
- Exécute `size target/release/myapp` avant et après. Si la section `.text` gonfle (par exemple, de 10 Ko à 100 Ko), reconsidère l'inlining des grandes fonctions.

### Alternatives
- Le déroulage de boucles ou la fusion d'itérateurs (abstractions à coût nul de Rust) peuvent obtenir des gains similaires sans inlining forcé.
- **Exemple** : Réécris `parse_stream` avec `fold` pour laisser le compilateur intégrer implicitement.

## Vérifier le résultat
### Benchmark
Avec criterion :

```rust
use criterion::{black_box, Criterion};
fn bench(c: &mut Criterion) {
    let data = vec![0x1234_5678; 1000];
    c.bench_function("inline_parse", |b| b.iter(|| parse_stream(black_box(&data))));
}
```

Compare avec et sans `#[inline(always)]` — attends une latence plus serrée.

### Assembleur
`cargo rustc --release -- --emit asm` montre `shr` et `and` dans la boucle, pas d'instructions d'appel.

### Vérification de la taille
`ls -lh` sur le binaire confirme une croissance minimale.

## Quand l'utiliser, et quand s'abstenir
J'utiliserais `#[inline(always)]` pour les petites fonctions chaudes comme `extract_bits` dans des boucles serrées, garantissant que la surcharge d'appel disparaît et que les optimisations se déclenchent. La surutilisation risque de gonfler les binaires et de ralentir les compilations, donc je profilerais pour la justifier, reviendrais à `#[inline]` ailleurs et surveillerais les effets sur le I-cache. Cela équilibre les gains de performance avec la maintenabilité et l'évolutivité dans une base de code Rust.
