[Swift] txtファイルのn行目をなるべく速く取得する

何MBもあるような巨大なtxtファイルのn行目だけを読み込みたい瞬間ってありますよね。
普通にやるなら、`String`として読み込んで、`string.components(separatedBy:"\n")[index]`か`string.split(separator:"\n", omittingEmptySubsequences: false)[index]`みたいな感じでなりそうですが、これだとファイル全体を分割しているので効率が悪そうです。
そこで実験してなるべく速くn行目を取得する方法を探してみました。
## 実験
用いたファイルは[青空文庫のリスト「公開中　作家別作品一覧拡充版：全て(CSV形式、zip圧縮）」](https://www.aozora.gr.jp/index_pages/person_all.html)で、ダウンロードした時点で17000行ちょい、サイズにして15MBほどあります。計測は
1. ファイルの読み込み
2. 分割
3. 1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000行目を同時に取得し、表示
という処理を行わせています。
### 1.`components`
```swift
func testPerformanceGetNthLineComponents() throws{
    self.measure {

        let string:String
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }

            string = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            string = ""
        }

        let splited = string.components(separatedBy: "\n")
        let strings = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].map{splited[$0]}
        print(strings)
    }
}
```
わかりやすくて良い方法で、平均0.192秒とそこそこのスピードです。

```
[Time, seconds] 
average: 0.192, 
relative standard deviation: 5.157%, 
values: [0.221948, 0.188873, 0.188767, 0.188911, 0.189229, 0.188864, 0.188640, 0.188775, 0.188801, 0.189360]
```

### 2. `enumerateLines`
文字列を1行ずつ取得できる`enumerateLines`を使えば、全体を分割せずとも必要な部分まで読んで`stop`すれば良いことになります。

```swift
func testPerformanceGetNthLineEnumerateLines() throws{
    self.measure {

        let string:String
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }

            string = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            string = ""
        }

        var indices = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].sorted().makeIterator()
        guard var target = indices.next() else{
            return
        }
        var count = 0
        var result:[String] = []
        string.enumerateLines(invoking: {line, stop in
            if target == count{
                result.append(line)
                if let _target = indices.next(){
                    target = _target
                }else{
                    stop = true
                }
            }
            count += 1

        })
        print(result)
    }
}
```
初回が異様に遅い(飛び値)ので平均では`components`に負けていますが、初回を除いた平均は0.101001と`components`より高速です。
XCTestでPerformance測定をすると初回が異様に遅くなりがちなのはなぜなんでしょうか……。

```
[Time, seconds] 
average: 0.362, 
relative standard deviation: 216.306%, 
values: [2.711354, 0.101003, 0.100871, 0.100783, 0.101008, 0.101088, 0.100787, 0.101444, 0.101147, 0.100880]
```
### 3.`Data`として読み込んで`withUnsafeBytes`で`split`する
`String`にする前に`split`してn行目を取ってしまう、というやり方です。

```swift
func testPerformanceGetNthLineDataAndWithUnsafeBytes() throws{
    self.measure {

        let data:Data
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }
            let url = URL(fileURLWithPath: path)
            data = try Data(contentsOf: url)

        } catch let error {
            print("ファイルが存在しません: \(error)")
            data = Data()
        }

        let bytes = data.withUnsafeBytes {
            $0.split(separator: UInt8(ascii: "\n"), omittingEmptySubsequences: false)
        }
        let result = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].compactMap{String(bytes: bytes[$0], encoding: .utf8)}
        print(result)
    }
}
```
何をやっているのかわかりづらくなった分、速度がグンと高速化します。平均0.039と、`enumuratedLines`の2.6倍、`components`の4.9倍高速です。

```
[Time, seconds] 
average: 0.039, 
relative standard deviation: 37.924%, 
values: [0.084047, 0.037760, 0.034003, 0.033994, 0.034018, 0.033920, 0.034091, 0.033933, 0.034028, 0.033961]
```

### 4.`Data`で`withUnsafeBytes`で1バイトずつ調べる
3の方法ではやはり全体を`split`していますが、この方法なら必要な行まで読んだ時点で`stop`できるのでその分高速化するはずです。

```swift
func testPerformanceGetNthLineDataAndWithUnsafeBytesAndScaning() throws{
    self.measure {

        let data:Data
        do{
            guard let path = Bundle(for: type(of: self)).path(forResource: "aozora", ofType: "csv") else {
                print("ファイルが存在しません")
                return
            }
            let url = URL(fileURLWithPath: path)
            data = try Data(contentsOf: url)
        } catch let error {
            print("ファイルが存在しません: \(error)")
            data = Data()
        }

        var indicesIterator = [1000, 2000, 5000, 8000, 10000, 12000, 15000, 17000].sorted().makeIterator()
        guard var targetIndex = indicesIterator.next() else{
            return
        }
        let bytes:[[UInt8]] = data.withUnsafeBytes {
            var results:[[UInt8]] = []
            var result:[UInt8] = []
            var count = 0

            for byte in $0{
                let isNewLine = byte == UInt8(ascii: "\n")
                if count == targetIndex && !isNewLine{
                    result.append(byte)
                }
                
                if count > targetIndex{
                    results.append(result)
                    result = []
                    if let _targetIndex = indicesIterator.next(){
                        targetIndex = _targetIndex
                        if count == targetIndex{
                            result.append(byte)
                        }
                    }else{
                        break
                    }
                }
                
                if isNewLine{
                    count += 1
                }
            }

            if !result.isEmpty{
                results.append(string)
            }

            return results
        }
        
        print(bytes.map{String(bytes: $0, encoding: .utf8)})
    }
}
```
平均で0.026秒と、`3`の方法よりも高速になりました。最初の`components`と比較すると7.4倍ほど高速化できたことになります。思いついた限りではこれが一番速い方法でした。

```
[Time, seconds] 
average: 0.026, 
relative standard deviation: 45.925%, 
values: [0.061011, 0.029828, 0.023706, 0.020109, 0.020274, 0.025639, 0.020205, 0.020277, 0.020406, 0.020213]
```

#### 10/2追記
```swift
if count == targetIndex && !isNewLine{
    result.append(byte)
}
↓↓↓↓↓
if count == targetIndex && !isNewLine{
    result.append(byte)
    continue   //count == targetIndexかつ!isNewLineなので、これ以降の行が呼ばれることはない。
}
```
とした方がパフォーマンスが上がるかな？と試したのですが、なんと`continue`がない方が速いという不思議な結果になりました。`continue`はなしでいきましょう。

```
[Time, seconds] 
average: 0.043, 
relative standard deviation: 31.214%, 
values: [0.082764, 0.040316, 0.038075, 0.038079, 0.037942, 0.038081, 0.038038, 0.037882, 0.038093, 0.038405]
```

## 結論
速度が必要な場合は4番の方法でいきましょう。もっと速く取得できる方法をご存知の方がいたら是非教えてください🙏🙏🙏

## 参考
* https://forums.swift.org/t/difficulties-with-efficient-large-file-parsing/23660