電気ひつじ牧場

技術メモ

ISUCON11のベンチマーカーを高速化

ISUCON12に向けて過去問をやっていたら、ISUCON11のベンチマーカーの実行に時間がかかっていたので高速化してみました。

このベンチマーカーはアプリケーションに負荷をかけ始めるまでの初期化処理に結構時間がかかります。 特にbench/random/image.goのinit()で大部分の時間を使っていました。

func init() {
    var files []fs.FileInfo

    var err error
    // 画像ファイル群の読み込み
    files, err = ioutil.ReadDir(imageFolderPath)
    if err != nil {
        log.Fatalf("%+v", fmt.Errorf("%w", err))
    }

    for i := 0; i < imageNum; i++ {
        fileInfo := files[rand.Intn(len(files))]
        //default.jpg以外の、.jpgで終わるファイルに限定する
        for fileInfo.Name() == "default.jpg" || !strings.HasSuffix(fileInfo.Name(), ".jpg") {
            fileInfo = files[rand.Intn(len(files))]
        }
        img, err := imgio.Open(filepath.Join(imageFolderPath, fileInfo.Name()))
        if err != nil {
            log.Fatalf("%+v", err)
        }
        img = adjust.Brightness(img, float64(rand.Intn(20)-10)/10.0/2)
        img = adjust.Contrast(img, float64(rand.Intn(20)-10)/10.0/2)
        img = adjust.Gamma(img, 0.1+rand.Float64()*3)
        img = adjust.Saturation(img, float64(rand.Intn(20)-10)/10.0/2)

        //encode
        buffer := new(bytes.Buffer)
        encoder := imgio.JPEGEncoder(rand.Intn(95) + 5)
        encoder(buffer, img)
        images[i] = buffer.Bytes()
    }
}

このコードでは、imageNum回の画像変換を直列実行しています。 次のようにCPUのコア数分だけ並列で変換処理することで高速化できます。

func init() {
    var files []fs.FileInfo

    var err error
    // 画像ファイル群の読み込み
    files, err = ioutil.ReadDir(imageFolderPath)
    if err != nil {
        log.Fatalf("%+v", fmt.Errorf("%w", err))
    }

    type imageInfo struct {
        name string
        i    int
    }

    ch := make(chan imageInfo, imageNum)
    for i := 0; i < imageNum; i++ {
        fileInfo := files[rand.Intn(len(files))]
        //default.jpg以外の、.jpgで終わるファイルに限定する
        for fileInfo.Name() == "default.jpg" || !strings.HasSuffix(fileInfo.Name(), ".jpg") {
            fileInfo = files[rand.Intn(len(files))]
        }
        ch <- imageInfo{fileInfo.Name(), i}
    }
    close(ch)
    var wg sync.WaitGroup
    for j := 0; j < runtime.NumCPU(); j++ {
        wg.Add(1)
        go func(ch chan imageInfo) {
            for c := range ch {
                img, err := imgio.Open(filepath.Join(imageFolderPath, c.name))
                if err != nil {
                    log.Fatalf("%+v", err)
                }
                img = adjust.Brightness(img, float64(rand.Intn(20)-10)/10.0/2)
                img = adjust.Contrast(img, float64(rand.Intn(20)-10)/10.0/2)
                img = adjust.Gamma(img, 0.1+rand.Float64()*3)
                img = adjust.Saturation(img, float64(rand.Intn(20)-10)/10.0/2)

                //encode
                buffer := new(bytes.Buffer)
                encoder := imgio.JPEGEncoder(rand.Intn(95) + 5)
                encoder(buffer, img)
                images[c.i] = buffer.Bytes()
                fmt.Println("image:", c.i)
            }
            wg.Done()
        }(ch)
    }
    wg.Wait()
}

(もう少し工夫すればchan imageInfoのバッファは不要のはず)

起動からベンチマーク開始までの時間はc6i.2xlargeのインスタンスで1m44s→39sと63%ほど速くなりました。

ベンチマーカーの周囲のコードを読んでないので、元の実装で何か意図があって並列化してないのか、さらに言えば画像を実行時に毎度変換処理する必要があるのかはよく分かっていません... とりあえず高速にベンチ回せるようになったのでこれで良いことにします。