Getting Started With Asm?


A_SN

Active Member
Joined
Jun 8, 2006
Messages
899
I need to optimize stuff in my game, and therefore I need to use ARM ASM. However I have no idea how to get started with it, nor do I know how to integrate it in a C file.

The first function that I need to optimize this way is a simple function that writes my final 32-bit color screen array to the 16-bit color framebuffer that currently takes 50% of the program's run time, it looks like this :

Code:
void fb_write(uint32_t *screen, uint16_t *fb)
{
	uint32_t i, p;

	for (i=0; i<76800; i++)	//76800 = 320 * 240
	{
		p = screen[i];
		fb[i] = ((p & 0x00f80000)>>19) | ((p & 0x0000fc00)>>5) | ((p & 0x000000f8)<<8);
	}
}

Now I'm not asking someone to do this for me in ASM so that I could just paste it in my program and not care about it anymore, because there's many other functions I'll have to optimize in ASM as well, so I need to learn how to do it, I'd just appreciate to be told about a few basics, being pointed to some essential ressources and to be given clues.

I know that Google is my friend but I consider you guys to be more reliable, and I might miss out if I don't ask anyone, plus this topic might be useful to other developers.
 
Before writing a single line of ASM, you really need to learn how to write efficient C, otherwise your asm code could as slow (if not slower) than the compiled C version!

So I'd urge you to look around the board for examples of efficient C coding - eg. using a loop that runs down to zero, rather than upwards (therefore letting the compiler miss out comparisons and just checking the Z flag), making use of the cache (do more per loop and branch less), and using the 32-bit nature efficiently (eg. writing 2 pixels instead of 1).

Once you code more like this and the code still isn't fast enough, then is the time to look at the generated assembler and see what you can further optimize.
 
What Squidge said.

I would also recommend you use gcc's -funroll-loops flag, I think it could do wonders for your loop.

A great book about coding optimized for ARM is "ARM System Developer's Guide" there are tons of hints and optimized primitive rutines to use for both C and ASM, you'll need to shell out some cash for it but if you want to learn it properly this is your best bet I think. I have it and I've learned a lot, although I haven't put it to good use yet.

PS. the compiler can work out 320 * 240 at compile time, there's no need to do it by hand. Let the compiler do it's job :).
 
First, great idea to learn ASM, whatever the reason.

Second : Isn't there a blitter that can do this (converting from 32 to 16bpp) inside the GP2X somewhere ?
Correct me, someone ?
 
I agree with Squidge and hammy_lite. The book that hammy_lite references is a good book on the ARM architecture and most of it's optimization are good.

The understand of how to do optimal C is basic understanding of the use of the architecture and can avoid the ASM trap (to be fast it must be in ASM). Letting the compiler help you craft optimum code is part of your task. But the task requires an understanding of how to help the compiler to a better job. Some of the optimum things for one architecture (for example ARM) are not that optimum for another (for example X86). If you know where your bottle necks are you can have architecture specific #ifdef's that will help the code run faster, that is if the code "needs" to run on multiple architectures.
 
Squidge posted on Dec 15 2006 at 01:35 PM said:
Before writing a single line of ASM, you really need to learn how to write efficient C, otherwise your asm code could as slow (if not slower) than the compiled C version!

So I'd urge you to look around the board for examples of efficient C coding - eg. using a loop that runs down to zero, rather than upwards (therefore letting the compiler miss out comparisons and just checking the Z flag), making use of the cache (do more per loop and branch less), and using the 32-bit nature efficiently (eg. writing 2 pixels instead of 1).

Once you code more like this and the code still isn't fast enough, then is the time to look at the generated assembler and see what you can further optimize.

According to your advices and to this paper (I've read the whole thing and tried applying it to my case as much as I could) I've modified my code into this :

Code:
void fb_write(uint64_t *screen, uint32_t *fb)
{
	uint64_t p;
	uint32_t i;

	for (i=320*240/2-1; i!=0; i--)
	{
		p = screen[i];
		fb[i] = ((p & 0x00f8000000000000ULL)>>35) | ((p & 0x0000fc0000000000ULL)>>21) | ((p & 0x000000f800000000ULL)>>8) | ((p & 0x0000000000f80000ULL)>>19) | ((p & 0x000000000000fc00ULL)>>5) | ((p & 0x00000000000000f8ULL)<<8);
	}
}

The only thing that I didn't apply was unrolling, because there's something that leaves me perplex about unrolling. Basically I'm supposed to unroll by an arbitrary factor? Like, copy the code in the loop an arbitrary number of times while incrementing inside the loop accordingly? Then how do I determine that amount of times?

Anyways, I've tested my old and new code under the same conditions (leaving it running) and during the same time (respectively 459.78 and 461.50 seconds, and that was random!) and with the old code the function took 59.59% of the time and with new code only 54.88%. I think it means my code is now 7.9% faster. That's already something, but not quite what I was looking for I must say :)

rixed posted on Dec 15 2006 at 02:10 PM said:
Second : Isn't there a blitter that can do this (converting from 32 to 16bpp) inside the GP2X somewhere ?
Correct me, someone ?

Well, maybe, but from the wiki page about using the blitter I couldn't tell how to use that in order to do what I want..

Gary Miller posted on Dec 15 2006 at 02:14 PM said:
If you know where your bottle necks are you can have architecture specific #ifdef's that will help the code run faster, that is if the code "needs" to run on multiple architectures.

No problem about that, I'm mainting portability while allowing myself to use any possible GP2X specific optimization using #ifdef's
 
Last edited by a moderator:
I don't think you'll get it much faster, it's pretty simple code so the compiler should do a good job of optimizing it. If you want it faster you'll either need figure out how to make the blitter do the depth conversion since this takes the most of your time or skip it completely (if that's an option for you).

I haven't used the blitter myself so I can't help you, but I think I'll read up on it, if I find anything that might help you I'll let you know.

However if this code uses up half of your time the rest of your code isn't very demanding, it should run fine below 100 MHz. So I think you've pushed it down enough already :).
 
hammy lite posted on Dec 16 2006 at 04:14 PM said:
I don't think you'll get it much faster, it's pretty simple code so the compiler should do a good job of optimizing it. If you want it faster you'll either need figure out how to make the blitter do the depth conversion since this takes the most of your time or skip it completely (if that's an option for you).

I haven't used the blitter myself so I can't help you, but I think I'll read up on it, if I find anything that might help you I'll let you know.

However if this code uses up half of your time the rest of your code isn't very demanding, it should run fine below 100 MHz. So I think you've pushed it down enough already :).

Dunno why but I have the feeling that it can be done much faster than that. Dunno what makes it that slow, mainly since I thought that & and | operations were free on ARM. Maybe memory copy? Well I'm sure we can make it faster.

As for the rest of my code, my program is in very early developement stages and although I get a decent frame rate (although I had to turn off the background star field for that), what I am for will be more demanding than that.
 
Last edited by a moderator:
Just figured out my function in ASM in its integrity, and I'm pretty glad I did, anyways, although I used -O3, there's lots of things the compiler has missed out.

For example, I got something like that :

Code:
mov	r5, #0
and	r3, r7, r5
mov	r3, r3, lsr #21

which I can simply erase and replace the next use of r3 by #0 since r3 will always be 0 in this case.

There's also other stuff I can simplify, like turning
Code:
mov	r6, #64512
and	r4, r8, r6
into
Code:
and	r4, r8, #64512

I have no idea how much I could save nor how could I put the modified ASM function into my C program, I'd like to be told how to do that. On a side note, on the 39 lines inside the function's loop, it seems that 33 are dedicated to the 32-bit color to 16-bit color transformation, and I'm pretty sure that I could get rid of almost half of them.

By the way there's something I couldn't figure out from all the documentation I looked at, if I have
Code:
orr	r3, r3, r4, asl #24
, does it mean r3 = (r3 | r4) << 24 or r3 = r3 | (r4 << 24)?
 
A_SN posted on Dec 16 2006 at 09:03 PM said:
Dunno why but I have the feeling that it can be done much faster than that. Dunno what makes it that slow, mainly since I thought that & and | operations were free on ARM. Maybe memory copy? Well I'm sure we can make it faster.

Shifts are free, & and | take 1 cycle.

As for the rest of my code, my program is in very early developement stages and although I get a decent frame rate (although I had to turn off the background star field for that), what I am for will be more demanding than that.

Ah ok, I should warn you though premature optimization can be a waste of time there will proberably be bigger fish to fry than this little bugger.

Don't let me discourage you though, learning and practicing optimization is a good idea.

A_SN posted on Dec 16 2006 at 10:32 PM said:
Code:
mov	r5, #0
and	r3, r7, r5
mov	r3, r3, lsr #21

which I can simply erase and replace the next use of r3 by #0 since r3 will always be 0 in this case.

I don't think it'll make a difference since moving between to registers and loading a constant will both take 1 cycle.

There's also other stuff I can simplify, like turning
Code:
mov	r6, #64512
and	r4, r8, r6
into
Code:
and	r4, r8, #64512

That constant is too large to store in the instruction itself so it'll have to be loaded from memory.
Or at least I think so, the limit is 2^13 or some such.

I have no idea how much I could save nor how could I put the modified ASM function into my C program, I'd like to be told how to do that. On a side note, on the 39 lines inside the function's loop, it seems that 33 are dedicated to the 32-bit color to 16-bit color transformation, and I'm pretty sure that I could get rid of almost half of them.

Comment it out in the C file (using a define or whatever), compile it and assemble your file containing your modified function then link the resulting object files as normal.

By the way there's something I couldn't figure out from all the documentation I looked at, if I have
Code:
orr	r3, r3, r4, asl #24
, does it mean r3 = (r3 | r4) << 24 or r3 = r3 | (r4 << 24)?

The later (I think).

Some of the stuff I written I might of pulled out of my ass because I'm tired :), but hopefully somebody will correct me if I'm mistaken. Either way I'll check up on stuff tomorrow.
 
Last edited by a moderator:
hammy lite posted on Dec 17 2006 at 01:10 AM said:
As for the rest of my code, my program is in very early developement stages and although I get a decent frame rate (although I had to turn off the background star field for that), what I am for will be more demanding than that.

Ah ok, I should warn you though premature optimization can be a waste of time there will proberably be bigger fish to fry than this little bugger.

Since this function takes me about 30 ms per frame, I must optimize it, and there's no way I could afford to have "a bigger fish to fry" than that.

Don't let me discourage you though, learning and practicing optimization is a good idea.

A_SN posted on Dec 16 2006 at 10:32 PM said:
There's also other stuff I can simplify, like turning
Code:
mov	r6, #64512
and	r4, r8, r6
into
Code:
and	r4, r8, #64512

That constant is too large to store in the instruction itself so it'll have to be loaded from memory.
Or at least I think so, the limit is 2^13 or some such.

Well, that would be useful if I could know for sure about that. I've looked up at comp.sys.arm and they say it's because you can't put a 32-bit constant in a 32-bit instruction (of course) but they don't say what's the limit, I guess it depends the opcode that goes with it. However if I can't use it as a constant I'm sure I can find myself a register to keep this value permanently, instead of mov'ing it in everytime, that's if I can dedicate a register to that tho. Can I use more registers than r0 to r8 tho?

Alternatively, since all my constants are full of zeroes (64512 is 0xFC00) couldn't I have "and r4, r8, #252, asl #8" ?

Comment it out in the C file (using a define or whatever), compile it and assemble your file containing your modified function then link the resulting object files as normal.

No way I'm gonna bother with that, what I want to know is how to integrate that in my C code using that asm() function :)

A_SN posted on Dec 16 2006 at 10:32 PM said:
Code:
mov	r5, #0
and	r3, r7, r5
mov	r3, r3, lsr #21

which I can simply erase and replace the next use of r3 by #0 since r3 will always be 0 in this case.

I don't think it'll make a difference since moving between to registers and loading a constant will both take 1 cycle.

No way! I refuse to believe that setting r5 to 0, ANDing r7 and r5 into r3 and shifting r3 in itself to then use r3 would be just as fast as replacing the next use of r3 by constant #0. This shift isn't even free, is it?


By the way, here's my modified ASM function. Went from 39 instructions in the loop to 18. Correct me if I got something wrong in my comments

Code:
fb_write:
	@ args = 0, pretend = 0, frame = 0
	@ frame_needed = 0, uses_anonymous_args = 0
	stmfd	sp!, {r4, r5, r6, r7, r8, lr}				store registers
	ldr	lr, .L245		@lr = 0x0004AFF8		original array start adress (local)
	ldr	ip, .L245+4		@ip = 38399			init of the counter
	add	lr, r0, lr		@lr += r0			calculation of the full adress
	mov	r0, r1			@r0 = r1			change of base adress
	mov	r4, #248
	mov	r6, #252
.L240:
	ldmia	lr, {r7-r8}		@r7 = LSW	r8 = MSW		example 0x008f8f8f and 0x001133ff (grey and red-orange)
	and	r2, r8, r4, asl #16	@r2 = r8 & (0xF8<<16)		r2 = MSW & 0x00F80000 = 0x00100000	//MSW red
	and	r3, r8, r6, asl #8	@r3 = r8 & (0xFC<<8)		r3 = MSW & 0x0000FC00 = 0x00003000	//MSW green
	mov	r1, r2, lsr #3		@r1 = r2 >> 3			r1 = 0x00100000 >> 3 = 0x00020000
	orr	r1, r1, r3, asl #11	@r1 = r1 | (r3 << 11)		r1 = 0x00020000 | 0x01800000 = 0x01820000
	and	r3, r8, r4		@r3 = r8 & 0xF8			r3 = MSW & 0x000000F8 = 0x000000F8	//MSW blue
	orr	r1, r1, r3, asl #24	@r1 = r1 | (r3 << 24)		r1 = 0x01820000 | 0xF8000000 = 0xF9820000
	and	r3, r7, r4, asl #16	@r3 = r7 & (0xF8<<16)		r3 = LSW & 0x00F80000 = 0x00880000	//LSW red
	orr	r1, r1, r3, lsr #19	@r1 = r1 | (r3 >> 19)		r1 = r1 | r3 = 0xF9820000 | 0x00000011 = 0xF9820011
	and	r3, r7, r6, asl #8	@r3 = r7 & (0xFC<<8)		r3 = LSW & 0x0000FC00 = 0x0000F800	//LSW green
	orr	r1, r1, r3, lsr #5	@r1 = r1 | (r3>>5)		r1 = r1 | r3 = 0xF9820011 | 0x000007C0 = 0xF98207D1
	and	r7, r7, r4		@r7 = r7 & 0x000000F8		r7 = LSW & 0x000000F8 = 0x00000088
	orr	r1, r1, r7, asl #8	@r1 = r1 | (r7 << 8)		r1 = r1 | (r7 << 8) = 0xF98207D1 | 0x00008800 = 0xF9828FD1
	str	r1, [r0, ip, asl #2]	@[r0 + (ip * 4)] = r1		write of r1 into the array
	subs	ip, ip, #1		@ip--				decrementation of ip
	sub	lr, lr, #8		@lr -= 8			decrementation of the original array pointer
	bne	.L240			@if (ip!=0) go back to .L240	loop condition
	ldmfd	sp!, {r4, r5, r6, r7, r8, pc}				@restore registers
.L240:	@original loop
	mov	r5, #0			@r5 = 0
	ldmia	lr, {r7-r8}		@r7 = LSW	r8 = MSW		//example 0x008f8f8f and 0x001133ff (grey and red-orange)
	mov	r4, #16252928		@r4 = 0x00F80000
	and	r3, r7, r5		@r3 = r7 & r5			r3 = LSW & 0x00000000 = 0	//LSW ? (USELESS)
	mov	r6, #64512		@r6 = 0x0000FC00
	and	r2, r8, r4		@r2 = r8 & r4			r2 = MSW & 0x00F80000 = 0x00100000	//MSW red
	mov	r3, r3, lsr #21		@r3 >>= 21			r3 = 0 >> 21 = 0
	and	r4, r8, r6		@r4 = r8 & r6			r4 = MSW & 0x0000FC00 = 0x00003000	//MSW green
	orr	r3, r3, r4, asl #11	@r3 = r3 | (r4 << 11)		r3 = r4 << 11 = 0x00003000 << 11 = 0x01800000
	mov	r5, #0			@r5 = 0
	mov	r1, r2, lsr #3		@r1 = r2 >> 3			r1 = 0x00100000 >> 3 = 0x00020000
	orr	r1, r1, r3		@r1 = r1 | r3			r1 = 0x00020000 | 0x01800000 = 0x01820000
	mov	r6, #248		@r6 = 0x000000F8
	and	r3, r7, r5		@r3 = r7 & r5			r3 = LSW & 0x00000000 = 0	//LSW ? (USELESS)
	and	r4, r8, r6		@r4 = r8 & r6			r4 = MSW & 0x000000F8 = 0x000000F8	//MSW blue
	mov	r3, r3, lsr #8		@r3 >>= 8			r3 = 0 >> 8 = 0				//USELESS
	orr	r3, r3, r4, asl #24	@r3 = r3 | (r4 << 24)		r3 = r4 << 24 = 0x000000F8 << 24 = 0xF8000000
	mov	r5, #16252928		@r5 = 0x00F80000
	orr	r1, r1, r3		@r1 = r1 | r3			r1 = 0x01820000 | 0xF8000000 = 0xF9820000
	mov	r6, #0			@r6 = 0
	and	r3, r7, r5		@r3 = r7 & r5			r3 = LSW & 0x00F80000 = 0x00880000	//LSW red
	and	r4, r8, r6		@r4 = r8 & r6			r4 = MSW & 0 = 0	//USELESS
	mov	r3, r3, lsr #19		@r3 >>= 19			r3 = r3 >> 19 = 0x00880000 >> 19 = 0x00000011
	orr	r3, r3, r4, asl #13	@r3 = r3 | (r4 << 13)		r3 = r3			//USELESS
	mov	r5, #64512		@r5 = 0x0000FC00
	orr	r1, r1, r3		@r1 = r1 | r3			r1 = r1 | r3 = 0xF9820000 | 0x00000011 = 0xF9820011
	mov	r6, #0			@r6 = 0
	and	r3, r7, r5		@r3 = r7 & r5			r3 = LSW & 0x0000FC00 = 0x0000F800	//LSW green
	and	r4, r8, r6		@r4 = r8 & r6			r4 = MSW & 0 = 0	//USELESS
	mov	r3, r3, lsr #5		@r3 >>= 5			r3 = r3 >> 5 = 0x0000F800 >> 5 = 0x000007C0
	orr	r3, r3, r4, asl #27	@r3 = r3 | (r4 << 27)		r3 = r3			//USELESS
	orr	r1, r1, r3		@r1 = r1 | r3			r1 = r1 | r3 = 0xF9820011 | 0x000007C0 = 0xF98207D1
	and	r7, r7, #248		@r7 = r7 & 0x000000F8		r7 = LSW & 0x000000F8 = 0x00000088
	orr	r1, r1, r7, asl #8	@r1 = r1 | (r7 << 8)		r1 = r1 | (r7 << 8) = 0xF98207D1 | 0x00008800 = 0xF9828FD1
	str	r1, [r0, ip, asl #2]	@[r0 + (ip * 4)] = r1		write of r1 into the array	//TODO use only ip
	subs	ip, ip, #1		@ip--				decrementation of ip
	mov	r4, r4, lsr #5		@r4 >>= 5			r4 = r4 >> 5 = 0	//USELESS
	sub	lr, lr, #8		@lr -= 8			decrementation of the original array pointer
	bne	.L240			@if (ip!=0) go back to .L240	loop condition
	ldmfd	sp!, {r4, r5, r6, r7, r8, pc}				@restore registers
.L246:
	.align	2
.L245:
	.word	307192
	.word	38399
	.size	fb_write, .-fb_write
	.section	.rodata.str1.4
	.align	2

Now how do I integrate it to my C function? Like this? (untested)

Code:
static inline void fb_write(uint64_t *screen, uint32_t *fb)
{
	uint64_t p;	//are these still necessary?
	uint32_t i;

	asm volatile
	(
	"ldr	ip, 38399\n"
	"mov	r4, #248\n"
	"mov	r6, #252\n"
".L240:\n"
	"ldmia	lr, {r7-r8}		@r7 = LSW	r8 = MSW		example 0x008f8f8f and 0x001133ff (grey and red-orange)\n"
	"and	r2, r8, r4, asl #16	@r2 = r8 & (0xF8<<16)		r2 = MSW & 0x00F80000 = 0x00100000	//MSW red\n"
	"and	r3, r8, r6, asl #8	@r3 = r8 & (0xFC<<8)		r3 = MSW & 0x0000FC00 = 0x00003000	//MSW green\n"
	"mov	r1, r2, lsr #3		@r1 = r2 >> 3			r1 = 0x00100000 >> 3 = 0x00020000\n"
	"orr	r1, r1, r3, asl #11	@r1 = r1 | (r3 << 11)		r1 = 0x00020000 | 0x01800000 = 0x01820000\n"
	"and	r3, r8, r4		@r3 = r8 & 0xF8			r3 = MSW & 0x000000F8 = 0x000000F8	//MSW blue\n"
	"orr	r1, r1, r3, asl #24	@r1 = r1 | (r3 << 24)		r1 = 0x01820000 | 0xF8000000 = 0xF9820000\n"
	"and	r3, r7, r4, asl #16	@r3 = r7 & (0xF8<<16)		r3 = LSW & 0x00F80000 = 0x00880000	//LSW red\n"
	"orr	r1, r1, r3, lsr #19	@r1 = r1 | (r3 >> 19)		r1 = r1 | r3 = 0xF9820000 | 0x00000011 = 0xF9820011\n"
	"and	r3, r7, r6, asl #8	@r3 = r7 & (0xFC<<8)		r3 = LSW & 0x0000FC00 = 0x0000F800	//LSW green\n"
	"orr	r1, r1, r3, lsr #5	@r1 = r1 | (r3>>5)		r1 = r1 | r3 = 0xF9820011 | 0x000007C0 = 0xF98207D1\n"
	"and	r7, r7, r4		@r7 = r7 & 0x000000F8		r7 = LSW & 0x000000F8 = 0x00000088\n"
	"orr	r1, r1, r7, asl #8	@r1 = r1 | (r7 << 8)		r1 = r1 | (r7 << 8) = 0xF98207D1 | 0x00008800 = 0xF9828FD1\n"
	"str	r1, [r0, ip, asl #2]	@[r0 + (ip * 4)] = r1		write of r1 into the array\n"
	"subs	ip, ip, #1		@ip--				decrementation of ip\n"
	"sub	lr, lr, #8		@lr -= 8			decrementation of the original array pointer\n"
	"bne	.L240			@if (ip!=0) go back to .L240	loop condition\n"
	"ldmfd	sp!, {r4, r5, r6, r7, r8, pc}				@restore registers\n"
	)
}

By the way, what about unrolling? Someone (Ukko) told me that the instructions cache could take 4 kB, therefore meaning that in order to obtain the best performance you shall unroll your loop so that it takes close to 1024 instructions. Can anyone confirm this?
 
Last edited by a moderator:
hammy lite posted on Dec 16 2006 at 03:14 PM said:
If you want it faster you'll either need figure out how to make the blitter do the depth conversion since this takes the most of your time or skip it completely (if that's an option for you).

Perhaps I'm missing something but wouldn't this be the answer. Currently every pixel in the screen buffer gets converted to then written to the frame buffer. Are you not just performing the same conversion calculations over and over again?

Why not work in the same depth as the frame buffer? I think this would also allow you to draw directly on the frame buffer. Then you would avoid both the conversion and copying screen[] to the frame buffer.

Or is there something I'm missing that would stop that working?
 
Last edited by a moderator:
stinch posted on Dec 17 2006 at 03:35 PM said:
hammy lite posted on Dec 16 2006 at 03:14 PM said:
If you want it faster you'll either need figure out how to make the blitter do the depth conversion since this takes the most of your time or skip it completely (if that's an option for you).

Perhaps I'm missing something but wouldn't this be the answer. Currently every pixel in the screen buffer gets converted to then written to the frame buffer. Are you not just performing the same conversion calculations over and over again?

Why not work in the same depth as the frame buffer? I think this would also allow you to draw directly on the frame buffer. Then you would avoid both the conversion and copying screen[] to the frame buffer.

Or is there something I'm missing that would stop that working?

Yes, I considered that, however back then I had no idea how expensive that conversion could be. Without knowing that, all I saw in using 32-bit color was advantages (sorted by decreasing order of importance) :

-An easier access to color layers (no need to use masks)
-The possibility to have a built-in alpha layer
-No real advantage in using 16-bit color (except reduced memory usage) since it's not any faster
-The possibility of being able to display anything in 24-bit color on a PC build of my app

Now that I know how unexpectedly expensive the conversion is I'm considering again doing it all in 16-bits, however it won't be the easiest thing ever to turn every of my functions from using 32-bits to using 16-bits.
 
Last edited by a moderator:
A_SN posted on Dec 17 2006 at 03:02 AM said:
Since this function takes me about 30 ms per frame, I must optimize it, and there's no way I could afford to have "a bigger fish to fry" than that.

I must have underestimated it's size, I was thinking it'd be at most ~50 cycles per iteration which would give ~10 ms per frame.

A_SN posted on Dec 17 2006 at 03:02 AM said:
A_SN posted on Dec 16 2006 at 10:32 PM said:
There's also other stuff I can simplify, like turning
Code:
mov	r6, #64512
and	r4, r8, r6
into
Code:
and	r4, r8, #64512

That constant is too large to store in the instruction itself so it'll have to be loaded from memory.
Or at least I think so, the limit is 2^13 or some such.

Well, that would be useful if I could know for sure about that. I've looked up at comp.sys.arm and they say it's because you can't put a 32-bit constant in a 32-bit instruction (of course) but they don't say what's the limit, I guess it depends the opcode that goes with it.

I checked it out and it seems that you can give it any bit-rotation of values up to 2^8, this is good news for your case, e.g. you should be able to give it 0x00fc0000 since its 0xfc rotated 16 to the left. This also means it could be 0xf000000f if you ever need it :).

However if I can't use it as a constant I'm sure I can find myself a register to keep this value permanently, instead of mov'ing it in everytime, that's if I can dedicate a register to that tho. Can I use more registers than r0 to r8 tho?

r0-r12 are always usable, r13 and r14 can be used in special cases but I'm not sure when.

Alternatively, since all my constants are full of zeroes (64512 is 0xFC00) couldn't I have "and r4, r8, #252, asl #8" ?

As I said above, yeah. But I don't think you need to shift it by hand this will be done by the assembler.

A_SN posted on Dec 16 2006 at 10:32 PM said:
Code:
mov	r5, #0
and	r3, r7, r5
mov	r3, r3, lsr #21

which I can simply erase and replace the next use of r3 by #0 since r3 will always be 0 in this case.

I don't think it'll make a difference since moving between to registers and loading a constant will both take 1 cycle.

No way! I refuse to believe that setting r5 to 0, ANDing r7 and r5 into r3 and shifting r3 in itself to then use r3 would be just as fast as replacing the next use of r3 by constant #0. This shift isn't even free, is it?

Oh, I must of miss-read it. You're are right after all :).

Code:
Now how do I integrate it to my C function? Like this? (untested)

[code]static inline void fb_write(uint64_t *screen, uint32_t *fb)
{
	uint64_t p;
	uint32_t i;

	asm volatile
	(
	"mov	r4, #248\n"
	"mov	r6, #252\n"
".L240:\n"
	"ldmia	lr, {r7-r8}		@r7 = LSW	r8 = MSW		example 0x008f8f8f and 0x001133ff (grey and red-orange)\n"
	"and	r2, r8, r4, asl #16	@r2 = r8 & (0xF8<<16)		r2 = MSW & 0x00F80000 = 0x00100000	//MSW red\n"
	"and	r3, r8, r6, asl #8	@r3 = r8 & (0xFC<<8)		r3 = MSW & 0x0000FC00 = 0x00003000	//MSW green\n"
	"mov	r1, r2, lsr #3		@r1 = r2 >> 3			r1 = 0x00100000 >> 3 = 0x00020000\n"
	"orr	r1, r1, r3, asl #11	@r1 = r1 | (r3 << 11)		r1 = 0x00020000 | 0x01800000 = 0x01820000\n"
	"and	r3, r8, r4		@r3 = r8 & 0xF8			r3 = MSW & 0x000000F8 = 0x000000F8	//MSW blue\n"
	"orr	r1, r1, r3, asl #24	@r1 = r1 | (r3 << 24)		r1 = 0x01820000 | 0xF8000000 = 0xF9820000\n"
	"and	r3, r7, r4, asl #16	@r3 = r7 & (0xF8<<16)		r3 = LSW & 0x00F80000 = 0x00880000	//LSW red\n"
	"orr	r1, r1, r3, lsr #19	@r1 = r1 | (r3 >> 19)		r1 = r1 | r3 = 0xF9820000 | 0x00000011 = 0xF9820011\n"
	"and	r3, r7, r6, asl #8	@r3 = r7 & (0xFC<<8)		r3 = LSW & 0x0000FC00 = 0x0000F800	//LSW green\n"
	"orr	r1, r1, r3, lsr #5	@r1 = r1 | (r3>>5)		r1 = r1 | r3 = 0xF9820011 | 0x000007C0 = 0xF98207D1\n"
	"and	r7, r7, r4		@r7 = r7 & 0x000000F8		r7 = LSW & 0x000000F8 = 0x00000088\n"
	"orr	r1, r1, r7, asl #8	@r1 = r1 | (r7 << 8)		r1 = r1 | (r7 << 8) = 0xF98207D1 | 0x00008800 = 0xF9828FD1\n"
	"str	r1, [r0, ip, asl #2]	@[r0 + (ip * 4)] = r1		write of r1 into the array\n"
	"subs	ip, ip, #1		@ip--				decrementation of ip\n"
	"sub	lr, lr, #8		@lr -= 8			decrementation of the original array pointer\n"
	"bne	.L240			@if (ip!=0) go back to .L240	loop condition\n"
	"ldmfd	sp!, {r4, r5, r6, r7, r8, pc}				@restore registers\n"
	)
}

Yeah I think that's how it's done. I've never done it myself though, so just give it a try and see what happens.

As for your modified code, I really don't feel like reading through all that at the moment. But maybe later, I am a bit curious why the code is so long to begin with.
 
Last edited by a moderator:
ok, for all I care about arm assembly:

shifts are free if you shift up to 3 at most, more than that take a whole cycle (don't ask me why, I was reading about arm timings and found that, also I'm not sure about which arm cpu was that as I don't recall)

hammy_lite is (now) right, you can load any immediate value from 0 to 255 and negatives (movn instruction) and use an appropriated shift to get values like 0xfc000000 or so..

I don't think that unrolling loops will make any difference, as contents on the code passed are still loaded into cache, so a short loop may be faster than leaving it unrolled and having cache to read-ahead everything, even why that's the idea of running code on gp2x's 940t: a short loop that could/should be cpu intensive so very little bus accesses are needed, the whole code takes only 4kb with no loops there should be nothing to do with it... but anyway, you could try that and tell us what you've figured out to make a proof of concept

oh yeah

and I think that if you write proper code to handle 2 pixels at a time and write it only ONCE on framebuffer (you can have a routine that sets a register lower/upper half depending on it's address and call it twice), even your code being slower the result should be a sensible speed gain, that's quite a job, but it's what optimizations are all about
 
Lint posted on Dec 18 2006 at 12:05 AM said:
and I think that if you write proper code to handle 2 pixels at a time and write it only ONCE on framebuffer (you can have a routine that sets a register lower/upper half depending on it's address and call it twice), even your code being slower the result should be a sensible speed gain, that's quite a job, but it's what optimizations are all about

I'm not sure to know what you mean, but isn't it what I'm already doing? I read 2 words at once (that are set in r7 and r8) and the resulting word matching to two 16-bit color pixels is stored in r1, which is then written to memory. I'm pretty sure that's what you mean, if so well it's already what I am doing.
 
Last edited by a moderator:
oh yeah
sorry about that, I thought that you don't
and still no speed gain for writing half of framebuffer?
 
The easiest way to mix asm and C is, IMHO, to write all asm code into a separate ASM file. You then have to declare global the functions you want to call from the C, and respect the C calling convention.

If you look for a good optimization (that the compiler cannot do) : write several words in a single instruction (strm). This reduces the number of cycle the write buffer has to wait for memory (and, IIRC, help fit more data in the write buffer).

This is of course easier to write words N by N if you know that you have a multiple of N to write.
 
Lint posted on Dec 18 2006 at 05:59 AM said:
oh yeah
sorry about that, I thought that you don't
and still no speed gain for writing half of framebuffer?

I do, about 7.9%. However that's with numerous useless operations being performed, because here it seems that the 12 AND and OR operations take quite some time compared to writing to memory.

rixed posted on Dec 18 2006 at 10:55 AM said:
The easiest way to mix asm and C is, IMHO, to write all asm code into a separate ASM file. You then have to declare global the functions you want to call from the C, and respect the C calling convention.

Not sure to understand how to do that, but I'll have to look for explanations on Google anyways, meanwhile if you know some good documentation for that, let me know

If you look for a good optimization (that the compiler cannot do) : write several words in a single instruction (strm). This reduces the number of cycle the write buffer has to wait for memory (and, IIRC, help fit more data in the write buffer).

This is of course easier to write words N by N if you know that you have a multiple of N to write.

Oh yeah, I talked with Ukko (RapidRacoon ASM dev) and he told me that he was working on a fast memory function that would use ldmia to load 9 words in 9 registers and write them all at once with stmia. So, unlike what I've done so far which consisted in using all the registers I could (pretty much) to treat my two words in parallel (is there any gain to doing that by the way or is it the same as doing one after the other?) I guess I should try to use as few registers as I can for each word so I can have as many registers I can to load and store results in?
 
Last edited by a moderator:
Lint posted on Dec 18 2006 at 12:05 AM said:
shifts are free if you shift up to 3 at most, more than that take a whole cycle (don't ask me why, I was reading about arm timings and found that, also I'm not sure about which arm cpu was that as I don't recall)

How can that be? The only reason I can think of is that the shift amount doesn't fit in the instruction and you'd have to shift by an amount stored in a register (which does costs one more cycle), but ALU instructions (e.g. ADD, SUB, AND, OR ...) have 8 bits to store the amount which is enough.

I would like some sources to satisfy my curiosity, if you don't mind.

I don't think that unrolling loops will make any difference, as contents on the code passed are still loaded into cache, so a short loop may be faster than leaving it unrolled and having cache to read-ahead everything, even why that's the idea of running code on gp2x's 940t: a short loop that could/should be cpu intensive so very little bus accesses are needed, the whole code takes only 4kb with no loops there should be nothing to do with it... but anyway, you could try that and tell us what you've figured out to make a proof of concept

Well, since the loop turned out to be this large loop unrolling will do very little and might even slow it down.
But if it were say, a 6 cycle loop with a 4 cycle overhead the overhead would be 40% of the loop and loop unrolling would speed it up A LOT.


A_SN posted on Dec 18 2006 at 12:47 PM said:
If you look for a good optimization (that the compiler cannot do) : write several words in a single instruction (strm). This reduces the number of cycle the write buffer has to wait for memory (and, IIRC, help fit more data in the write buffer).

This is of course easier to write words N by N if you know that you have a multiple of N to write.

Oh yeah, I talked with Ukko (RapidRacoon ASM dev) and he told me that he was working on a fast memory function that would use ldmia to load 9 words in 9 registers and write them all at once with stmia. So, unlike what I've done so far which consisted in using all the registers I could (pretty much) to treat my two words in parallel (is there any gain to doing that by the way or is it the same as doing one after the other?) I guess I should try to use as few registers as I can for each word so I can have as many registers I can to load and store results in?

Lint's suggestion would help, but only by about as much as the first 2 pixels at a time optimization. Worth a shot though.

I guess the aim is to to load as many as possible while still keeping all constants and addresses in registers.
 
Last edited by a moderator:
Back
Top