👌

Radix Sortで浮動小数点数をソートする!

2023/07/27に公開

https://colab.research.google.com/github/t-fuchi/RadixSort/blob/main/RadixSort.ipynb

Radix Sortで浮動小数点数をソートする!

皆さん、基数ソート(Radix Sort)はご存知ですね?O(kN)のすごいやつです。実はMacに搭載されているBSDのsortコマンドは、--radixsortオプションがあったりします。ただし、このコマンドは数値には使えないとマニュアルに書いてあります。もともと文字列用に考案されたものだからでしょうか。そんな中、こんな記事を見つけました。整数はもとより、浮動小数点数でも基数ソート出来るよ〜という記事です。詳しくは記事を読んでいただくとして、この記事にはソースコードが付いています。これは早速ダウンロードしてColab上で性能検証したい!ということでやってみました。

乱数の生成

乱数を10億個生成します。20分以上かかります。

%%time
from random import random

with open('input', 'w') as fout:
    for _ in range(1000000000):
        print((random() - 0.5) * 1000000, file=fout)
CPU times: user 23min 45s, sys: 36.8 s, total: 24min 22s
Wall time: 24min 37s

sortコマンド

比較のため、sortコマンドでソートして、時間を測定します。40分近くかかります。

%%time
!sort -n input > output_sort
CPU times: user 11.2 s, sys: 1.37 s, total: 12.6 s
Wall time: 39min 30s
!wc -l output_sort
!head output_sort
1000000000 output_sort
-499999.99974544795
-499999.99890903855
-499999.9982175315
-499999.9977990386
-499999.9960182383
-499999.9955448005
-499999.99452409195
-499999.9943637242
-499999.9931208382
-499999.9897155051

ちゃんとソートできています。

std::sort

STLのsortを使ってソートしてみます。

%%writefile /content/std_sort_vector.cpp

#include <stdio.h>
#include <stdlib.h>
#include <chrono>
#include <vector>
#include <algorithm>

int main(int argc, char *argv[])
{
    char buf[BUFSIZ];
    std::vector<double> input;

    auto start = std::chrono::system_clock::now();
    while (fgets(buf, BUFSIZ, stdin) != NULL) {
        input.push_back(atof(buf));
    }
    auto end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "load: %f sec\n", elapsed / 1000);

    start = std::chrono::system_clock::now();
    std::sort(input.begin(), input.end());
    end = std::chrono::system_clock::now();
    elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "sort: %f sec\n", elapsed / 1000);

    start = std::chrono::system_clock::now();
    for (size_t i = 0; i < input.size(); i++)
    {
        printf("%.12lf\n", input[i]);
    }
    end = std::chrono::system_clock::now();
    elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "out:  %f sec\n", elapsed / 1000);
}
Writing /content/std_sort_vector.cpp
!g++ -Ofast std_sort_vector.cpp
%%time
!./a.out < input > output_std_vector
load: 287.233000 sec
sort: 152.108000 sec
out:  1266.370000 sec
CPU times: user 7.85 s, sys: 876 ms, total: 8.73 s
Wall time: 28min 26s
!wc -l output_std_vector
!head output_std_vector
1000000000 output_std_vector
-499999.999745447945
-499999.998909038550
-499999.998217531480
-499999.997799038596
-499999.996018238307
-499999.995544800477
-499999.994524091948
-499999.994363724196
-499999.993120838189
-499999.989715505100

IOの時間が26分で同じとすると、sortコマンドのソートの時間は15分です。STLのsortは2分半ですから相当早い。速いとは聞いていましたが、浮動小数点数に特化しているのも効いている可能性があります。sortコマンドは高機能な分、余計な処理が挟まっているのかもしれません。

ska sort

それでは基数ソートを測定しましょう。

%cd /content
!git clone https://github.com/skarupke/ska_sort.git
/content
Cloning into 'ska_sort'...
remote: Enumerating objects: 16, done.[K
remote: Total 16 (delta 0), reused 0 (delta 0), pack-reused 16[K
Receiving objects: 100% (16/16), 14.25 KiB | 7.13 MiB/s, done.
Resolving deltas: 100% (4/4), done.
%%writefile /content/ska_sort.cpp
#include <stdio.h>
#include <stdlib.h>
#include <vector>
#include <chrono>
#include "ska_sort/ska_sort.hpp"

int main(int argc, char *argv[])
{
    char buf[BUFSIZ];
    std::vector<double> input;
    double f;

    auto start = std::chrono::system_clock::now();
    while (fgets(buf, BUFSIZ, stdin) != NULL) {
        input.push_back(atof(buf));
    }
    auto end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "load: %f sec\n", elapsed / 1000);

    start = std::chrono::system_clock::now();
    ska_sort(input.begin(), input.end());
    end = std::chrono::system_clock::now();
    elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "sort: %f sec\n", elapsed / 1000);

    start = std::chrono::system_clock::now();
    for (size_t i = 0; i < input.size(); i++)
    {
        printf("%.12lf\n", input[i]);
    }
    end = std::chrono::system_clock::now();
    elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    fprintf(stderr, "out:  %f sec\n", elapsed / 1000);
}
Writing /content/ska_sort.cpp
!g++ -Ofast ska_sort.cpp
%%time
!./a.out < input > output_ska
load: 279.663000 sec
sort: 59.707000 sec
out:  1266.158000 sec
CPU times: user 7.29 s, sys: 839 ms, total: 8.13 s
Wall time: 26min 46s
!wc -l output_ska
!head output_ska
1000000000 output_ska
-499999.999745447945
-499999.998909038550
-499999.998217531480
-499999.997799038596
-499999.996018238307
-499999.995544800477
-499999.994524091948
-499999.994363724196
-499999.993120838189
-499999.989715505100

速い。sort部分の処理時間はSTLのsortの半分以下です。IOを含めたコマンドとしては5%ほどの時間短縮ですが、内部で浮動小数点数のソートが必要なプログラムにはとても有効なことが分かりました。

Discussion