#C and Assembler
10 messages in this thread
Recently, a time critical loop motivated me to take a look (MC68000 User's
Manual in hand) at the assembly source output of the Manx compiler for the
first time.
This is not intended to be critical of Manx. However, even to a novice, it
is pretty easy to see places where the efficiency of the compiler output
can be improved. I was able to decrease the execution time of the above
mentioned loop by 25% by making just a handful of tweaks to the code. For
most of the programs I write, this kind of tweaking probably isn't worth
the effort and, I'm certainly not inclined to start writing entire programs
in assembler. Once in a while however, it appears that the benifits of
altering the compiler output or using in-line assembly code in your C
functions can be dramatic.
Examining the compiler output is also very educational. C gives us many
alterative ways of performing the same task. The code generated by these
alternatives can differ significantly in size and efficiency. Finally,
looking at the compiler output is a good way, although perhaps not a great
way, to begin learning 68000 assembly language.
I was surprised that Manx's "optimizing" compiler doesn't detect divisions
by powers of two. I'd always been led to believe that other compilers
(MicroSoft, for example) did do this. Aztec C alway uses the CPU's divide
instructions instead of shifting. Is this a reasonable thing to do? Does
the 68000 do power of two divides as fast as it can shift?
I'd like to hear some other people's thoughts on and experiences with
mixing C and assembler.
Interesting… I just started about a month or so ago looking at the
asm output of Manx as well… and optimizing certain time-critical loops,
etc. In fact, as I read your message, it was as if I had written it :-).
I've learned a lot about asm from it, but as you mentioned, a side-effect
of looking at the output can teach you a great deal about how to write
better C (for the Manx cmpiler anyway… and in general a lot of times). I
don't have an answer on your dived-shift query, but as an aside… Manx's
assembler is an optimizing assembler, you might get to know 'db' and/or
'sdb' and take a browse at what the 'final' output is (ie. disassemble it
from mem). Btw, I now write some routines, take the output and re-write
the loops and/or change register usage (re-writing the whole routine at
times) then link it in seporately, but I don't use in-line asm (haven't
found a need to yet). – Keith
I believe that the Manx assembler only substitutes bsr for jsr and short
branches for long branches where possible and removes unnecessary movem
instructions. Frankly, that's not much. I was hoping that the compiler
would be alittle smarter.
The compiler seems to treat each C statement as if it existed in a vacuum.
It never takes advantage of results obtained in previous statements unless
you explicitly write the C code to do it. This was the most important
lesson I learned. It is well worth storing difficult to calculate
intermediate results if they will be used more than once. Even the values
of multidimentional array elements fall into this category.
It was interesting to see the effects that using register variables had.
Here's a question for you, if you're using a register variable as an array
index, should it be a UBYTE, a USHORT or a ULONG? Assume, of course, that a
UBYTE can represent the range of the index.
Your msg caught my attention because I write 68000 assembly language five
days a week at work and only write C on my Amiga.
Regarding the last question in your msg: No the 68000 (and 68010) do
shifts much faster than divides, even divides by powers of two. (The 68020
has a barrel shifter to speed up shifts even more!) The exact timing of
the divide instruction seems hard to predict, but I haven't seen times less
about 110 CPU clocks. The timing of the shift instructions is easily
computed: 2 clocks per bit plus 6 if byte or word; 2 more clocks if long.
For small shifts the difference between shift and divide can be a factor of
ten or more.
In general, I've noticed that C compilers produce pretty lousy code, at
least in comparison with the compilers fr, say, FORTRAN that I've seem on
mainframes. In part, this is because C is so low-level: if you want to do
a shift, you can use the shift operator. However, I don't see much use of
even simple optimizations I learned in college, like strength reduction,
moving invariant code out of loops, removing dead code, to say nothing of
global flow optimization.
All that notwithstanding, it's probably a bad idea to mix assembly language
and C unless the CPU speed is critical. You reduce readability and
portability. Even if you halve CPU execution time, you probably reduce the
wall-clock time by much less, unless you're doing ray-tracing or some
such.
Thanks for the info on division vs shifting. As other messages point out,
there is, of course, a problem with negative numbers. However, many times
you are only dealing with positive numbers. The compiler could "easily"
recognize unsigned variables.
People alway point to FORTRAN compilers as examples of efficient code
generators. They should be. They've been tweaking them for over 20 years.
🙂
I agree that the C philosophy may be to let the programmer select the most
efficient method to solve a problem. I just wasn't aware of how seriously
they ment it. And as I said, I thought MicroSotf C did do optimizations
like replacing multiplication and division by shifting where possible.
Although I've been happy with Aztec C for over there years, it's starting
to look a little lame compared to Lattice 5.0.
Since I don't have/use Manx, I don't have the "luxury" of in-line asm,
nor or easily tweaking the generated assembly code before assembling it.
However, even if I had this ability, I wouldn't use it. I really think
it's a bad habit to develop and sloppy technique.
I have no problem with analyzing the way the compiler generates code,
and learning to use C code constructs that "help" the compiler to
optimize. I also have no problem with mixing C code with assembler code –
I do it frequently. But the assembler modules I use _belong_ in a separate
file from the C code. This assists the maintainability and portability of
the code. Functions within a program should be either ALL C or ALL asm –
never mixed. Inline assembler allows you to violate this (I know that you
can have the entire function as inline assembler code, but it is still
easier to maintain if it is kept in a separate file).
As to taking the assembler output from the compiler and "tweaking" the
code (hand-optimizing) – that works fine until you have to make a change to
the logic in that module. Which version do you change? How do you
remember which "tweaks" you made to the previous version? Which are still
relevant? Major maintenance hassles! On top of which, if I have determined
that there is a performance requirement for a certain module to be coded in
assembler, I can almost always do a much better job of optimization if I
simply code that function from the start in assembler. There's a price
that you pay for using the higher-level language. You're just trying to
short-change it, while I'm opting not to pay it. It'll cost you more now
(performance-wise), and the time you save now (by letting the compiler do
the preliminary leg-work) will be spent later three-fold when you have to
modify your program. All code, unless it's a quick-'n'-dirty, throwaway,
will need to be changed eventually — that's the nature of software.
But I really don't have any opinions on this subject. <grin>
arithmetic right shifts are NOT divides by powers of two for negative
numbers.
"Arithmetic right shifts are NOT divides by powers of two for negative
numbers."
Am I missing something here? Is there something peculiar to the 68000 that
I don't know about? (Probably… I've done VERY little 68000 assembly.)
A negative number is nominally 2's complement notation with the MSB (sign
bit) lit. An ASR shifts the byte/word right N bits, replicating the MSB as
the shift occurs. In any assembly language I've ever used and any
reference I've ever seen, this is a divide by 2^N.
What do you know that I don't?
Yup, you're missing something here. Not peculiar to the 68000, peculiar to
the way 2's complement numbers work. As this was one of the exercises the
C class just did, you may want to drop by some Thursday night, Room 3, 01PM
EST (7PM PST) just to find out what we know (and you do to, but seem to
have forgotten). See ya soon.
Your question made me realize that I didn't know the answer for certain. I
had always just intuitively figured that there was something fishy about
shifting negative numbers. So after scratching ones and zeros on a pad for
a while, here's the scoop. Shifting negative numbers works fine as long as
the number is a multiple of the "divisor". In other words, as long as you
only shift out zeros. If the number isn't a multiple, then trucating
produces different results. 5 shifted right 1 bit is 2. -5 shifted right
one place is -3. I guess you could still call this division by 2. However,
it's not associative. (-1 * n) / 2 doesn't equal -1 * (n / 2). Still,
sometimes you might not care.