【C言語】ポインタ(16)「ポインタを使ったほうが配列を使うより実行スピードが速い」は本当か?

どうも。gochaです。

これまでも述べてきているように、下記の本はポインタを理解するのに適した素晴らしい本です。私がポインタをある程度本格的に教える立場の場合迷わず、この本を購入してもらい教材として使います。が、今回は記載が本当かコードを書いて確かめていきます。

上記の本のPage 63 に下記のような記述があります。

昔はポインタ演算を駆使した方が高速なプログラムを書くことができた、ということです。配列はたいていの場合でぐるぐる回して色々な処理をするための使います。通常はこんな形になりますね。

for (i=0; i < LOOP_MAX; i++){
//
// ここにarray[i] を使ったいろいろな処理が入る。
// array[i]は、何度も登場する。
//
}

array[i]は、ループ中に何度も出現するわけですが、その度ごとに*(array + 1) に相当する足し算を行っていたのでは、当然効率は悪くなります。
それに対し、ポインタ演算を使用した以下のプログラムでは、

for (p=&array[0]; p != &array[LOOP_MAX]; p++){
//
// ここに(*p) を使ったいろいろな処理が入る。
// (*p) は、何度も登場する。
//
}

*pがループの中に何度出現しても、加算はループの終わりの1回だけで済みます。
K&R p.119には「ポインタを使う方が一般に高速である」という記述がありますが、これがその根拠であると思われます。
しかしこれはあくまで昔の話です。

現在のコンパイラは最適化がすすんでおり、ループの中の共通部分式の括りだしという作業は、コンパイラの最適化の基本です。現在の通常のCコンパイラであれば、配列、ポインタのどちらを使用しても、効率にはっきりさが出るようなことはまずありません。まったく同じ機械語コードを出力ことも多いものです。

参照:p63 『C言語ポインタ完全制覇』前橋 和弥著

上記が本当かどうか確かめてみましょう。下記がコードです。

#include <stdio.h>
#include <time.h>
#include <math.h>
#include <stdlib.h>

#define LOOP_MAX 1000000

int main (void){
	unsigned long int i;
	int array[LOOP_MAX];
	int pointer[LOOP_MAX];
	int *p;

	clock_t a_start = clock();
	for ( i = 0 ; i < LOOP_MAX ; i++){
		array [i] = i + i * i;
		array [i] = (i + i * i) / (i+1) - array[i];
		printf("# array [ %d ] = %d \n", i , array[i]); 
		
	}
	clock_t a_end = clock();
	
	clock_t p_start = clock();
	for ( i = 0 ; i < LOOP_MAX ; i++){
		*(pointer + i) = i + i * i;
		*(pointer + i) = (i + i * i) / (i+1) - *(pointer+i);
		printf("#  *(p + %d)  %d \n", i , *(pointer+i) ); 
	}
	clock_t p_end = clock();

	double elapsed_a = (double)(a_end - a_start ) * 1000.0 / CLOCKS_PER_SEC;
	double elapsed_p = (double)(p_end - p_start ) * 1000.0 / CLOCKS_PER_SEC;

	printf ("### Time elapsed in ms : array : %lf\n" , elapsed_a);
	printf ("### Time elapsed in ms : pointer : %lf\n" , elapsed_p);

	return 0;
}

実行するためのスクリプトはこちら。

#! /bin/bash -f

rm -rf  *.exe *.out

gcc -o normal-opt.exe p63.c 
./normal-opt.exe

gcc -o o5.exe -O5 p63.c 
./o5.exe

log にエラーを含めて出力結果を書き込んでいます。(尚、上記のプログラムは要点を単純化するため、いろんな厳密性を犠牲にしてます。例えばWall Clock Time と CPU Time の議論等。)

もしそれらについて、より詳細なコメントがほしい場合は、ぜひ、「お問い合わせ」からご連絡ください。今回の実行環境は、下記のとおりです。

Host PC : Dell Precision 7710:
Host OS : Windows 10 64 bit
Host CPU: Intel(R) Core(TM) i7-6820HQ CPU@2.70GHZ 2.70GHz
RAM 64GB
VMware player : VMware Workstation 15 Player>Guest OS :
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.6 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.6 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
GCC version
$ gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

で、log の結果がこちら(最終行周辺です)。

Time elapsed in ms : array : 313.631000
Time elapsed in ms : pointer : 324.481000

あれ、Array(配列)の方が実行時間短いですね。

もう一回実施してみた結果がこちらです。

Time elapsed in ms : array : 316.772000
Time elapsed in ms : pointer : 296.033000

今度は、Array(配列)の方が大きく(時間がかかる)なりました。

上記から言えることは、配列もポインターも大して実行時間に差分は無く、時と場合によっては、配列の方が速いケースもあるということですね。

そのため、本の記載は、現時点で、今回のgochaの前提条件では正しそう、ということが言えるかなと判断してます。

なお、実行手順を一通り動画にまとめました。下記チェックしてみてください。

今日はここまで。