Swiftのnilについて考えてみた

by

ブログアプリイメージ

初めまして、金井 大朗と申します!

先日社内にてSwiftのnilとは何なのかという話題が出て口頭で説明しようとしても漠然とnullみたいなもの?としか出てこなかったので改めて調べてみました。

このあたりは興味がある人は既にQiita等で見たことがあるかも知れません。

まずはOptional型について

Optional型とは、nil、もしくは別の値を持つことができる型のことを指します。

Optional型ではない変数にnilをセットすることはできません。

var hoge: String? = nil             // OK
var fuga: Optional<String> = nil    // OK
var piyo: String = nil              // コンパイルエラー

Optional型の定義は型の後に?をつけるOptional<型>のように定義することができます。

ではこのOptional型とは一体何者なのでしょうか。

GitHubに公開されているSwiftのOptional.h型のコードを見てみましょう。

template <typename T, bool = is_trivially_copyable<T>::value>
class OptionalStorage {
  union {
    char empty;
    T value;
  };
  bool hasVal;

こちらはC++なので見慣れない方がいるかも知れませんが、OptionalStorageというクラスに渡された型を保持するTとTの値を持っているかどうかのbool値を持つクラスになります。

unionの意味はchar型とT型の変数を同じメモリ空間に割り当てるという意味になり、この場合はchar型か渡されたT型のサイズの大きな方をメモリに割り当てることになります。

nilとは?

nilを代入する処理を探す前に力尽きてしまったので以下は推測になります。

恐らくnilとは、Tの実態が定義されておらずhasValがfalseの状態のことを指しているのではないでしょうか。

ここで一つ疑問が出てきました。nilを代入したとしてもOptional型という実体は定義されているようなのでメモリは消費されるのではないでしょうか?

試してみた

簡易的なメモリチェック用のプロジェクトを作成して検証してみます。

単純にボタンが配置されたStoryBoardとボタンを押した際にOptional型の配列にnilを1024 * 1024個追加するものを用意しました。

メモリ使用量の計測関数については以下の関数を利用させて頂いています。

    var hoge: [Bool?] = []
    var counter = 0;
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print(String(format:"Bool? Size: %d" ,MemoryLayout<Bool?>.size))
        print(String(format:"UInt8? Size: %d" ,MemoryLayout<UInt8?>.size))
    }

    // ボタンが押された際に配列にnilを1024 * 1024個追加する処理
    @IBAction func TapButton(_ sender: Any) {
        counter += 1
        print(String(format: "%d回目", counter))
        printMemory(header: "StartMemory:")
        for i in 0..<1024 * 1024 {
            hoge.append(nil)
        }
        printMemory(header: "EndMemory:")
    }
    
    // メモリ表示文を整形
    func printMemory(header: String) {
        if let memory = getMemoryUsed() {
            print(String(format: "%@%dMB", header , memory))
        }
    }
    
    // 使用者が単位を把握できるようにするため
    typealias MegaByte = UInt64

    // 引数にenumで任意の単位を指定できるのが好ましい e.g. unit = .auto (デフォルト引数)
    func getMemoryUsed() -> MegaByte? {
        // タスク情報を取得
        var info = mach_task_basic_info()
        // `info`の値からその型に必要なメモリを取得
        var count = UInt32(MemoryLayout.size(ofValue: info) / MemoryLayout<integer_t>.size)
        let result = withUnsafeMutablePointer(to: &info) {
            task_info(mach_task_self_,
                      task_flavor_t(MACH_TASK_BASIC_INFO),
                      // `task_info`の引数にするためにInt32のメモリ配置と解釈させる必要がある
                      $0.withMemoryRebound(to: Int32.self, capacity: 1) { pointer in
                        UnsafeMutablePointer<Int32>(pointer)
                      }, &count)
        }
        // MB表記に変換して返却
        return result == KERN_SUCCESS ? info.resident_size / 1024 / 1024 : nil
    }

今回はBool?型とUInt8?型にて検証を行いました。

Bool型とUInt8型はそれぞれ1バイトなのでそれぞれTに値する部分が1バイト、hasValのboolが1バイトの合計2バイトの予想でしたが、MemoryLayoutにてサイズを計測した結果

Bool? Size: 1
UInt8? Size: 2

UInt8?型は想定通りでしたがBool?型は予想と違う結果になりました。

以下Bool?とUInt8?にnilを代入した際のメモリ消費量を12回試行した結果になります。

Bool? UInt8?
開始時メモリ 追加後メモリ 開始時メモリ 追加後メモリ
1回目 77MB 80MB 77MB 83MB
2回目 80MB 83MB 83MB 89MB
3回目 83MB 84MB 89MB 91MB
4回目 84MB 89MB 91MB 101MB
5回目 89MB 90MB 101MB 103MB
6回目 90MB 91MB 103MB 105MB
7回目 91MB 92MB 105MB 107MB
8回目 92MB 101MB 107MB 125MB
9回目 101MB 102MB 125MB 127MB
10回目 102MB 103MB 127MB 129MB
11回目 103MB 104MB 129MB 131MB
12回目 104MB 105MB 131MB 133MB

やはりnilを代入してもメモリは消費されるようです。Bool?にtrue、Uint8?に10を代入してもメモリ消費量に変化はなかったためnilを代入した時点でT分のメモリも確保されているようです。

2バイト*1M個なので2MBずつ上がるかと予想していましたが所々メモリ消費量が急上昇していたり2MB以下の上昇量だったりしています。

この辺りは配列処理やメモリ管理系で別に動いているものがあるのかも知れませんね。

金井 大朗
iPhone Androidアプリ、サーバサイド、インフラなど広く浅くなエンジニアです。

Egg Device Application

東京品川のスマホアプリ製作・開発会社です。
一般アプリ業務用アプリからVRアプリまで開発可能。

求人情報

スマホアプリ製作・開発の
相談を受け付けています

メールでのご相談

お電話でのご相談
TEL 03-5422-7524
平日10:00~18:00