はじめに

この記事は、Arduinoで独自のライブラリを書いていて「使っていない関数までフラッシュメモリを圧迫しているのでは?」と不安に感じている開発者向けです。
Arduino IDEは「使われていないコードはリンクしない」と謳っていますが、自作ライブラリでも本当にその仕組みが働くのかを検証します。記事を読み終える頃には、実際にどの関数がリンクされるかをobjdumpnmコマンドで確かめられるようになり、メモリを意識したライブラリ設計が行えるようになります。

前提知識

  • Arduinoスケッチの基本(.inoファイルの書き方)
  • C++でクラスと名前空間の扱いに慣れていること
  • ターミナル(PowerShell / bash)でカレントディレクトリを移動できること

Arduino IDEのリンク戦略と「Dead Code Elimination」の限界

Arduino IDE(1.8.x/2.x)は、コンパイル時に-ffunction-sections -fdata-sectionsと、リンク時に--gc-sectionsを暗黙に有効にしており、到達不能な関数・変数を除外する「Dead Code Elimination(DCE)」を掛けています。しかし、この最適化は「コンパイル単位(.c/.cppファイル)」で動作するため、1ファイル内に多数の関数を詰め込むと、使っていない関数であっても同じセクションに入っていると削除されないという落とし穴があります。自作ライブラリをヘッダオンリーで実装したり、全てをstatic関数にしていると、予期せぬフラッシュ消費が発生する可能性があるのです。

実験:使っていない関数がリンクされるか検証してみる

ステップ1 検証用ライブラリの作成

libraries/TestLib/以下に以下3ファイルを作ります。

libraries/TestLib/
├── TestLib.h
├── TestLib.cpp
└── examples/Verify/Verify.ino

TestLib.h

Cpp
#pragma once #include <Arduino.h> namespace TestLib { void used(); // スケッチから呼ぶ void unused(); // 呼ばない }

TestLib.cpp

Cpp
#include "TestLib.h" void TestLib::used() { Serial.println(F("used")); } void TestLib::unused() { // 巨大なテーブルを持つ const uint16_t tbl[1024] = {0}; Serial.println(F("unused")); for (auto v : tbl) Serial.println(v); }

examples/Verify/Verify.ino

Cpp
#include <TestLib.h> void setup() { Serial.begin(9600); TestLib::used(); } void loop() {}

ステップ2 ビルドしてバイナリを覗く

Arduino IDEで「スケッチ→検証/コンパイル」を選んだ直後、ビルドフォルダ(/tmp/arduino-sketch-XXXXなど)へ移動します。

Bash
$ cd /tmp/arduino-sketch-XXXX $ arm-none-eabi-nm -S -t d Verify.ino.elf | grep TestLib

出力例:

00001234 0000000e T _ZN7TestLib4usedEv
00001300 00000820 T _ZN7TestLib6unusedEv   // 820h = 2080Byte

usedunusedもシンボルが残っており、後者は定数テーブル含め2 KB以上フラッシュを消費していることがわかります。

ステップ3 最適化を効かせる改造

TestLib.cppを以下のように書き換え、関数を別セクションに配置します。

Cpp
__attribute__((optimize("-Os"))) __attribute__((section(".text.used"))) void TestLib::used() { ... } __attribute__((section(".text.unused"))) void TestLib::unused() { ... }

更に、呼び出し側が存在しないunused__attribute__((weak))を付けて再度ビルド。

$ nm -S Verify.ino.elf | grep TestLib
00001234 0000000e T _ZN7TestLib4usedEv
                    w _ZN7TestLib6unusedEv   // シンボルは弱い参照、サイズ0

.text.unusedセクションは--gc-sectionsで削除され、フラッシュ使用量が減ったことが確認できます。

ハマった点と補足

  • ヘッダオンリーライブラリ(.hのみ)を使うと全てインライン展開されるため、使っていない関数でもテンプレートの暗黙的インスタンス化で膨らむことがある
  • static関数はGCされやすいが、外部から参照されないinline関数も同様
  • Windows版Arduino IDEではビルドフォルダが%LOCALAPPDATA%\Temp\arduino-sketch-XXXXに作られるため、PowerShellでcd $env:LOCALAPPDATA\Tempから探すと早い

解決策まとめ

  1. ライブラリ内部で「必ず使う関数」と「使わないかもしれない関数」を明確に分離する
  2. 後者を__attribute__((weak))section(".text.xxx")で独立させ、--gc-sectionsで削除対象にする
  3. クラス実装を.cppに分離し、利用側が#includeするヘッダは最小限に留める
  4. ビルド後はnm/size/avr-objdump -hでセクションサイズを常にチェックし、想定外の肥大を早期に発見する

まとめ

本記事では、Arduino IDEが標準で有効にしているDead Code Eliminationがファイル単位で動作することを実験で示し、自作ライブラリで使っていない関数がリンクされるケースとその回避策を紹介しました。

  • Arduino IDEの-ffunction-sections -fdata-sections--gc-sectionsは基本的に有効
  • 1ファイルに複数関数を詰め込むと、使っていないものまでセクションに含まれ、削除されない
  • section属性やweak属性を活用し、不要な関数を独立したセクションに配置することでフラッシュを節約できる

これで「ライブラリを追加したらフラッシュが突然不足した」という悲劇に見舞われることは減るはずです。次回は「ESP32のSPIFFSを使ってプログラムのOTA更新を実現する方法」について掘り下げていきます。

参考資料