So, SUPER low-level what does an IF() look like, how is it handled by an x86 processor?
CPU Operations The four primary functions of a processor are fetch, decode, execute and write back. Fetch- is the operation which receives instructions from program memory from a systems RAM. Execute- is where the operation is performed. Each part of the CPU that is needed is activated to carry out the instructions.
The CPU is an electronic machine that works on a list of computer things to do, called instructions. It reads the list of instructions and runs (executes) each one in order. A list of instructions that a CPU can run is a computer program. The clock rate, or speed of a CPU's internal parts, is measured in hertz (Hz).
Benchmarks. When buying a new PC, you can check the CPU's name to get its age and approximate performance level. The blurb should also tell you the clock speed and whether it's a dual core or quad-core chip. More GHz and more cores are always better, though a dual-core chip can still be faster than a quad-core.
The three logical units that make up the central processing unit are the arithmetic and logic unit (ALU), main storage, and the control unit. Main storage retains active programs and data. It is relatively expensive, so secondary storage is used to store programs and data until they are needed in main storage.
The processor has "Branch if" instructions that when a certain condition is met it branches, and otherwise it continues on to the next instruction.
So
if(A)
{
dosomething;
}
would become
load A into register 0
if the zero flag is set (ie, register 0 contains 0x00) then jump to endcondition)
dosomething
endcondition:
More complex conditions ( if(A || B && C)
) become a sequence of instructions that leaves a register in a 0 or non zero state, so the branchif instruction can jump or not jump based on the conditional flags.
There are many conditional flags (zero, carry, negative, overflow, etc), and some branchif instructions also operate on more complex conditions (ie, it might actually check to see if a register is equal to another register, rather than simply looking at flags). Each architecture is different and makes tradeoffs so the instruction set is complete, but also speedy and compact.
As moocha points out in the comments, some architectures allow you to apply a conditional to some, many, or even all instructions, so you might not only have 'branch if' instructions, but also 'and if', 'add if', 'move if' etc.
The x86 is very, very, very complex beyond this simple explanation once you get into pipelining, out of order execution, caching, microcode, and all the other advanced topics. For most purposes the above explanation is sufficient. If you're writing a hand crafted very, very tightly wound algorithm, though, you'll have to take these things into account for maximum performance and throughput.
That's a topic for another question though...
-Adam
It's fairly easy to use the output of a C compiler (use the -S
switch on gcc) to see what output a given snippet of C will generate when compiled. Be careful when using optimisation on toy programs though. If you're not careful the optimiser will often optimise away conditionals that will always go one way or another (see this article on microbenchmarks for a more detailed explanation).
For example, a trivial C program:
#include <stdio.h>
int main (int argc, char **argv) {
int ii = 10;
int jj = 20;
if (jj > ii) {
puts ("jj > ii \n");
}
return 0;
}
compiles to the following assembly language:
.file "foo.c"
.section .rodata
.LC0:
.string "jj > ii \n"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl $10, -8(%ebp)
movl $20, -12(%ebp)
movl -12(%ebp), %eax
cmpl -8(%ebp), %eax
jle .L2
movl $.LC0, (%esp)
call puts
.L2:
movl $0, %eax
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2"
.section .note.GNU-stack,"",@progbits
For a brief dissection of what's going on:
The first section (.rodata
) declares a constant with the string 'jj > ii \n
')
The second section is initialising the contents of the ii
and jj
variables on the stack.
The bit from cmpl -8(%ebp), %eax
is doing the actual comparison; the jle
instruction is skipping over the call to 'puts
', which is effectively the logic of the 'if
' statement reversed.
After the label '.L2
' the system is tidying up the top of the stack and returning from the call.
It's a branch instruction, dependent on the specific machine architecture. It figures out how to set up a memory location or register to test for a specific low-level condition - like branch-if-not-equal or branch-if-not-zero, ... -- does that test then jumps (or doesn't if the condition fails) to another part of memory. Obviously if you have a complex condition it may need to do evaluate many different conditions and may involve several branch instructions.
Generally the CPU has what is called an Instruction register, which holds the memory address of the current machine language opcode to be executed next... and numerous other registers to hold data.
Generally, after the cpu executes each opcode in the the instruction register, it simply increments it by one to move to the next position in memory which should have the next opcode in the compiled program application.
One opcode (actually there are probably several), however allows the cpu to "Branch", by "comparing" the values in two other cpu registers, and if one is greater than the other, it copies one memory address into the instruction register, whereas if the other is the largest, it copies a second, different memory address into the instruction register.
That's about as "low" level as it can be put it w/o talking about relays and transistors...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With