質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ソート

複数のデータを、順序性に従って並べ替えること。 データ処理を行う際に頻繁に用いられ、多くのアルゴリズムが存在します。速度、容量、複雑さなどに違いがあり、高速性に特化したものにクイックソートがあります。

Q&A

解決済

4回答

743閲覧

未ソートデータとソート済データをソートプログラムに入れた場合の、実行にかかる時間の比較

zazenbo

総合スコア8

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

ソート

複数のデータを、順序性に従って並べ替えること。 データ処理を行う際に頻繁に用いられ、多くのアルゴリズムが存在します。速度、容量、複雑さなどに違いがあり、高速性に特化したものにクイックソートがあります。

0グッド

0クリップ

投稿2018/06/25 15:27

解決したい疑問点

環境は、Windows7, eclipse を使っています。

一方向連結リストをソートするプログラムに、未ソートデータとソート済データを入力した場合について、プログラムの実行時間を比較したところ、未ソートデータのほうが時間がかかったのですが、なぜそうなるのかがわかりません。

下記ソースコードの概要は、
・gpa,credit,name の3要素から構成される生徒の成績データが、input.txtファイルに
....................
3.5 40 Taro
2.1 60 Hanako
.
.
....................
のような形で数万人分記録されていて、それを input 関数で構造体の連結リストに入力する。
・input.txtファイルとoutput.txtファイルの場所はコマンドライン引数で入力。
・gpa,credit,name のいずれでソートするかもコマンドライン引数で入力。今回は name でソート。
・ソートは昇順。最大値抽出法を使用。
・ソート部分の実行時間を知るため、sort 関数を clock 関数で囲んでいる。
・入力データには、バラバラの未ソートデータおよび、すでにソート済のデータの2つを使用し、比較する。

該当のソースコード

C

1 2#include <stdio.h> 3#include <stdlib.h> 4#include <string.h> 5#include <time.h> 6 7typedef struct school_record SRec; 8struct school_record{ 9 float gpa; 10 int credit; 11 char name[200]; 12 SRec *next; 13}; 14 15SRec* input(SRec* p, char* fname1); 16void output(SRec* p, char* fname2); 17int gpa_comp(const void* val1, const void* val2); 18int credit_comp(const void* val1, const void* val2); 19int name_comp(const void* val1, const void* val2); 20SRec* listsort(SRec* header, int (*comp)(const void*, const void*)); 21SRec* sort(SRec* header, char* key); 22 23 24 25int main(int argc, char *argv[]) 26{ 27 SRec* header = NULL, *Sheader; 28 clock_t clk_start, clk_end; 29 30 // コマンドライン引数チェック 31 if(argc != 4){ 32 fputs("コマンドライン引数の数が間違っています.\n", stderr); 33 exit(0); 34 } 35 if(strcmp(argv[1],"gpa") && strcmp(argv[1],"credit") && strcmp(argv[1],"name")){ 36 fputs("1つめのコマンドライン引数は, gpa, credit, name のいずれかにしてください.\n", stderr); 37 exit(0); 38 } 39 40 header = input(header, argv[2]); 41 42 clk_start = clock(); // sort関数の処理時間を計測 43 Sheader = sort(header, argv[1]); 44 clk_end = clock(); 45 46 output(Sheader, argv[3]); 47 48 printf("%f", (double)(clk_end - clk_start) / CLOCKS_PER_SEC); 49 50 return 0; 51} 52 53 54 55 56SRec* input(SRec* header, char* fname1) 57{ 58 float gpa; 59 int credit; 60 char name[200]; 61 SRec *p; 62 SRec **tail = &header; 63 64 // ファイルのオープン 65 FILE *fp; 66 if ((fp = fopen(fname1, "r")) == NULL){ 67 fprintf(stderr, "%sのオープンに失敗しました.\n", fname1); 68 exit(1); 69 } 70 71 // データをリストへコピー 72 while( fscanf(fp, "%f%d%s", &gpa, &credit, name) != EOF){ 73 74 // 領域確保 75 if( (p = (SRec*)malloc(sizeof(SRec)) ) == NULL){ 76 fprintf(stderr, "データ領域の確保に失敗しました.\n"); 77 exit(1); 78 } 79 // 一時変数からリストへ書き込み 80 p->gpa = gpa; 81 p->credit = credit; 82 strcpy(p->name, name); 83 p->next = NULL; 84 85 // リストの末尾に要素を追加 (tail は header か next を指すポインタ) 86 *tail = p; 87 tail = &(p->next); 88 } 89 fclose(fp); 90 return header; 91} 92 93 94 95void output(SRec* p, char* fname2) 96{ 97 SRec* tmp; 98 99 // ファイルのオープン 100 FILE *fp; 101 if ((fp = fopen(fname2, "w")) == NULL){ 102 fprintf(stderr, "%sのオープンに失敗しました.\n", fname2); 103 exit(1); 104 } 105 // データの書き込み 106 while( p != NULL ){ 107 fprintf(fp, "%.1f %3d %s\n", p->gpa, p->credit, p->name); 108 tmp = p; 109 p = p->next; 110 free(tmp); 111 } 112 fclose(fp); 113} 114 115 116 117 118 119 120// gpa比較関数 121int gpa_comp(const void* val1, const void* val2) 122{ 123 return ( (*(SRec*)val1).gpa < (*(SRec*)val2).gpa ) ? -1 : ( (*(SRec*)val1).gpa == (*(SRec*)val2).gpa ) ? 0 : 1 ; 124} 125 126// credit比較関数 127int credit_comp(const void* val1, const void* val2) 128{ 129 return ( (*(SRec*)val1).credit < (*(SRec*)val2).credit ) ? -1 : ( (*(SRec*)val1).credit == (*(SRec*)val2).credit ) ? 0 : 1 ; 130} 131 132// name比較関数 133int name_comp(const void* val1, const void* val2) 134{ 135 return strcmp( (*(SRec*)val1).name, (*(SRec*)val2).name); 136} 137 138 139 140// ソートを行う関数 141SRec* listsort(SRec* header, int (*comp)(const void*, const void*)) 142{ 143 SRec* Sheader = NULL, *q; 144 SRec** p, **max; //header か next を指すポインタ 145 146 // 元リストが空になるまで以下を繰り返し 147 while( header != NULL ){ 148 p = &header, max = &header; 149 150 // 元リストの最大値を探す 151 while( *p != NULL ){ 152 if( comp(*max, *p) < 0 ){ 153 max = p; 154 } 155 p = &( (*p)->next ); 156 } 157 // 元リストの最大値を削除して、新リストの先頭に挿入 158 q = Sheader; 159 Sheader = *max; 160 *max = (*max)->next; // 削除完了 161 Sheader->next = q; // 挿入完了 162 } 163 return Sheader; 164} 165 166 167// keyに対応する比較関数をlistsort関数へ振り分ける 168SRec* sort(SRec* header, char* key) 169{ 170 SRec* Sheader = NULL; 171 172 if(strcmp(key, "gpa") == 0){ 173 Sheader = listsort(header, gpa_comp); 174 } 175 if(strcmp(key, "credit") == 0){ 176 Sheader = listsort(header, credit_comp); 177 } 178 if(strcmp(key, "name") == 0){ 179 Sheader = listsort(header, name_comp); 180 } 181 182 return Sheader; 183} 184 185 186 187 188

結果

     入力データ
データ数   未ソート ソート済
.............................................
10000個   0.70秒   0.76秒
20000個   3.02秒   2.91秒
40000個   13.58秒  12.96秒
50000個   24.72秒  21.22秒
60000個   41.66秒  34.53秒
80000個   86.76秒 68.59秒
100000個   147.48秒  111.96秒
..............................................

となりました。ソートは問題なく行われます。
データ数が多くなると、ソート済データのほうが実行時間がかなり早いです。

なぜなのか

listsort 関数内に関して、ループを回す回数は未ソートとソート済で変わらないはずです。未ソートとソート済で違うところがあるとすれば、それは

C

1// 元リストの最大値を探す 2 while( *p != NULL ){ 3 if( comp(*max, *p) < 0 ){ 4 max = p; 5 } 6 p = &( (*p)->next ); 7 }

のif文が、未ソートの場合は実行される時とされない時があり、ソート済の場合は毎回実行される、ということだと思います。
ですから、むしろソート済のほうが実行時間が長くなるはずだと思うのですが・・・

なぜ未ソートのほうが時間がかかるのか、教えてください。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

結論から言うと、データの局所性の影響によるものだと考えられます。

まず、アセンブルリストを出力してどのようにコンパイルされたのかを確認したところ、比較部分でcmovsを使っていたので、ソート済みとソートなしとではまったく同じパスが実行される、つまり、その部分では速度に差が出ないということが判りました。

となると、考えられるのはデータの局所性の影響です。1要素のサイズが216バイト(64bit環境の場合)で、1万件だと2,160,000バイト、つまり、2MBちょっとということで、そこそこ大きいです。実際にはヒープの管理領域も含まれるのでそれよりも若干大きくなるでしょう。とはいえ、デスクトップ向けのCPUなら十分キャッシュに収まります。2万件だと4MB超。CPUのモデルによってはそろそろ収まりきれないかもしれません。4万件だと8MB超。ハイエンド向けのCPUでもキャッシュから溢れます。
キャッシュから溢れた場合は、次にそのデータを読むときはシステムメモリ(DRAM)から読み出すことになりますが、その際、プリフェッチという仕組みが働き、ある程度まとまったサイズの連続した領域を「先読み」します。先読みした領域はキャッシュに保存されるので、DRAMよりも遥かに高速に読み書きできます。

ここで「局所性」が効いてくるわけです。順番がバラバラだと、先読みした領域に次に使いたいデータが存在する確率は低いですが、順番が整っている(ソート済みの)データだと、次に使いたいデータが先読みした領域に存在する可能性は極めて高くなります。
キャッシュが有効に働くというわけです。

ソート済みのデータの方が実行時間が短いのは、そして、件数が多いほどより顕著に表れるのはそのためではないかと思われます。

投稿2018/06/26 16:32

catsforepaw

総合スコア5938

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

zazenbo

2018/06/27 03:35

初学者ゆえ知らないことだらけでしたが、わかりやすい説明のおかげで理解することができました。 他の方もわかりやすかったのですが、最も詳しい説明だったのでベストアンサーにさせて頂きます。 回答ありがとうございました!
guest

0

コンパイラ・CPUの仕様によりますが
アセンブリ言語レベルですと
CPUの高速化(パイプライン)の都合で条件分岐の予測をCPUが間違えると遅くなります。

ループ内の条件分岐ですので、CPUが前回の分岐と同方向を予測するアルゴリズム
もしくは、常に分岐すると予測するアルゴリズムであった場合
ソート済みか否かで速度が変わる要因になります。


なんとなくですが、三項演算子を用いる事でcmov命令に最適化されそうな気がします。

c

1max = comp(*max, *p) < 0 ? p : max;

投稿2018/06/25 16:41

編集2018/06/25 16:42
asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

zazenbo

2018/06/27 07:14

CPUが効率的に処理してくれるから本来の時間計算量以上の性能が出たわけですね。 三項演算子は試してみましたが、結果は同じでした…。 回答ありがとうございました!
guest

0

未検証なので自信はないのですが、「参照の局所性」とか呼ばれたりする現象ではないかと思います。
ソート済みの方がメモリアクセスする範囲が集中しますので、メモリのキャッシュが有効に働いて効率的になるからではないかと思います。

投稿2018/06/25 16:39

segavvy

総合スコア958

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

zazenbo

2018/06/27 07:17

「参照の局所性」、知らなかったですが、たしかに影響していそうですね。 回答ありがとうございました!
guest

0

ソート時と未ソート時ではcomp関数の実行時間が違うからでは

投稿2018/06/25 15:38

y_waiwai

総合スコア87774

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

zazenbo

2018/06/27 07:23

comp関数の実行時間、実質的には strcmp 関数の実行時間ということですかね? 未ソートとソート済で実行時間が変わる理由が思いつかないです…
y_waiwai

2018/06/27 07:34

まあ、ソート済みであれば常に最小の文字比較で終わるけど、ソートされてなければ、(それに比べ)長い文字の比較となるってとこですな。 strcmp関数を自分で実装するとなるとどういうふうに組むか、を考えるとわかると思います
zazenbo

2018/06/27 16:13

たとえば未ソートなら taro と hanako のような比較が多く、ソート済なら tarao と taro のような比較が多くなると思います。 strcmp関数では文字列の1文字目から順に大きさを比較し、大きさが違う文字があった時点でreturnするので、未ソートの taro と hanako では1文字目でreturn、ソート済の tarao と taro では4文字目でreturnとなると思います。ですからその意味でもやはり、未ソートのほうが早く終わるように思えてしまうのですが・・・
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問