[net.lang.c] A BETTER way to do Multidimensional arrays in C

srw@lasspvax.UUCP (Steve White) (01/13/85)

A few months ago I realized there was a better way to do multidimensional
arrays in C.  This way is not new; in fact, it is discussed on page 110
of Kernighan and Ritchie.  But after reading the discussions in net.lang.c
concerning the subject of multidimensional arrays I decided a lot of 
people might not be aware of it or might not appreciate its advantages.
Here's how it works.

The following program illustrates the idea (go ahead, try it!):

main()
	{
	double	**array;		/* This will be a 10 x 12 array */
	int i;

	array = (double **) calloc(10,sizeof(double *)); 	
	/* Number of rows = 10 */

	for	(i = 0; i < 10; i++)
		array[i] = (double *) calloc(12,sizeof(double));  
	/* Number of columns = 12 */

	array[2][3] = 3.14159265359;
	array[4][5] = 2.71828182846;
	anyfunc(array);
	}

anyfunc(a)
double **a;
	{
	printf("%.14g  %.14g\n",a[2][3],a[4][5]);
	}

We first allocate space for an array of pointers, the length of which is
the number of rows.  Then each of these pointers is set to point to 
enough space for one row of the array. Once this is done, then we can
pretend we have a regular 2-dimensional array, except that we never have
to specify its dimensions to any subroutines that use it.

There are a couple of DISADVANTAGES to the above method:

	1. It uses slightly more storage.

	2. You have to initialize the pointers, as in the above program.

It has, however, lots more ADVANTAGES:

	1. All storage allocation can be done dynamically.

	2. Subroutines don't need to know the dimensions of the array to
	access its elements.

	3. Non-rectangular arrays are as easy as rectangular.

	4. The indices do not have to start at zero! Say we wanted our
	indices to start at 1 instead of 0 in the above program (as in
	Fortran). Then the initialization would be changed to

	array = (double **) calloc(10,sizeof(double *)) - 1; 	
	for	(i = 1; i <= 10; i++)
		array[i] = (double *) calloc(12,sizeof(double)) - 1;  

	5. It is FASTER than the standard way (since there is no multiplication
	needed to calculate the address).

To test the relative speeds of this way and the standard way, I ran the
following programs:

/* Old way */
main()
	{
	double	array[200][200];
	register int i, j, l;

	for	(l = 0; l < 10; l++)
		for	(i = 0; i < 200; i++)
			for	(j = 0; j < 200; j++)
				array[i][j] = array[j][i];
	}


/* New way */
main()
	{
	double	**array;
	register int i, j, l;

	array = (double **) calloc(200,sizeof(double *)); 	
	for	(i = 0; i < 200; i++)
		array[i] = (double *) calloc(200,sizeof(double));  

	for	(l = 0; l < 10; l++)
		for	(i = 0; i < 200; i++)
			for	(j = 0; j < 200; j++)
				array[i][j] = array[j][i];
	}

The "Old way" took 22 sec. on our Vax 11/750, while the "New way"
took 17.4 sec. (When the optimizer was invoked with -O, the times were
21.4 and 16.2 sec., for the old and new ways, respectively.)

I think this way of handling multidimensional arrays is definitely
superior to the standard way, at least where speed and flexibility
are important.
								Hoping this was useful to you
								(or at least interesting),

								Steve White
								Cornell Univ. Dept. of Physics

gwyn@brl-tgr.ARPA (Doug Gwyn <gwyn>) (01/15/85)

Steve's suggestion of using vectored-row matrices is a good one;
indeed I think I'll include it in the matrix package in our support
library.  There need to be alloc/free routines (with alloc taking
care of setting up the vectors) but for large matrices it would
be worth it since double indirection will usually be faster than
index arithmetic.

This brings me to one of my oldest gripes:  There are not enough
standardized programming support routines in the C library.
Everyone ends up inventing his own set of routines for common
operations, such as complex arithmetic or queueing.  There have
been a few useful additions recently but there should be more.

Bob Larson <BLARSON%ECLD@usc-ecl.ARPA> (01/15/85)

Of course we could all use Bliss-10, which did this automatically :-)
Only one instruction is needed per array reference, indexing and indirection
were set up as part of the table.  (What? Your computer doesn't have 
indexed indirect indexed indirect addressing?   Are you sure your bytes are
variable size and that you word size is 36 bits? :-)

Sorry for the digression,  (I must be getting senile at 24)

Bob Larson
-------

rpw3@redwood.UUCP (Rob Warnock) (01/15/85)

+---------------
| A few months ago I realized there was a better way to do multidimensional
| arrays in C.  This way is not new; in fact, it is discussed on page 110
| of Kernighan and Ritchie...
| 	Steve White
| 	Cornell Univ. Dept. of Physics
+---------------

If my memory serves me, in the Algol-60 community these arrays of pointers
to arrays are called "Illiffe vectors" or "dope vectors". The PDP-10 Algol
compiler used them, and due to the PDP-10's ability to indirect-and-index
through memory, you could access a multi-dimensional array in ONE instruction,
if the subscripts were in registers (as they often are in inner loops).

(The best I could do on a 68000 was 3n+1 instructions, where "n" is the
number of dimensions in the array, and 4n+1 if you don't want to destroy
the subscripts. Could someone who knows the VAX comment on the use of
Illiffe vectors on that machine?)

For example, suppose we have a three-dimensional array declared "A[17,12,25]".
Then let memory location "A_" be the first word of a 17-word array of addresses
of the second-level pointers. In each element of "A_" the indirect bit will be
turned on, and the index field will name (say) register "T2". To assist the
description, call the (normally nameless) second-level arrays A_0 through A_16.
Then "A_" is:

	OPDEF	Z	[0]	;allow the index and indirect syntax to work.

	A:	Z	@A_0(T2)	;A_0 indexed by T2, then indirect
		Z	@A_2(T2)	; through memory.
		...
		Z	@A_16(T2)
	
Then the second-level pointers are 12-word vectors (using A_13 as an example):

	A_13:	Z	A_13_0(T3)	;note that the 2nd level uses T3
		Z	A_13_1(T3)	; and since it's the last level
		...			; of pointers, no indirect bit ("@").
		Z	A_13_11(T3)

And on the final level are just the array elements themselves:

	A:				;also the address of the "real" A
	A_0_0:	 BLOCK	25		;size of third dimension
	A_0_1:	 BLOCK	25
		 ...
	A_13_11: BLOCK	25
	A_14_0:	 BLOCK	25
		 ...
	A_16_11: BLOCK	25

Now put the first subscript in register T1, the second in T2, and the third
in T3. Then to load register T0 with A[T1,T2,T3], simply say:

		MOVE	T0,@A_(T1)	;could also be an ADD, FMUL, etc.

That's nice!

On a 68000, the same "instruction" is:

		lshl	#2,d1		;multiply indices by 4
		lshl	#2,d2		; (plus more work if you needed to
		lshl	#2,d3		;  save them for later)
		addl	#A_,d1
		movl	d1,a0
		addl	a0@,d2
		movl	d2,a0
		addl	a0@,d3
		movl	d3,a0
		movl	a0@,d0

(Unfortunately, when I tried this on the C compiler I use, even with "-O"
it didn't realize you could "addl a0@,d?". It did "movl a0@,d0" instead,
and then added the index to d0, before moving it to a0. Oh well...)

ANyway, Illiffe vectors are the way to go for *speed*. They do cost you
a size penalty, which can be small if the last dimension is large. How
messy would it be to get the compiler to allocate the vectors for you?
(...for static arrays only, please!) Or if you don't want to fool with
the compiler, a simple pre-processor (driven from pragmats) could compute
the vectors and the initializers for them.

Rob Warnock
Systems Architecture Consultant

UUCP:	{ihnp4,ucbvax!dual}!fortune!redwood!rpw3
DDD:	(415)572-2607
USPS:	510 Trinidad Lane, Foster City, CA  94404