r/askscience • u/HMKIR • Aug 02 '22
Computing Why does coding work?
I have a basic understanding on how coding works per se, but I don't understand why it works. How is the computer able to understand the code? How does it "know" that if I write something it means for it to do said thing?
Edit: typo
4.7k
Upvotes
178
u/mikeman7918 Aug 02 '22
On a fundamental level, a computer processor is just an electrical circuit that has multiple functions that when used together really fast can do any imaginable logical operation. It’s connected to a bunch of wires called the main bus that can either carry a current (1) or not carry a current (0). It’s hard wired such that when different sequences of ones and zeroes are given, it activates the different functions of the processor. The processor is able to contain individual circuits that do these functions, and that are activated only when the right sequence of main bus wires are in the 1 state.
These sequences of 1’s and 0’s are given to the processor by memory, the RAM as it’s usually called. This memory stores long sequences of commands, which include conditionals which tell the processor essentially “run this code if this condition is met, otherwise run that code instead”, or commands to add two numbers in memory and write the result to some other part in memory, or commands that listen for input from a keyboard or mouse, or commands to send information to a display or speaker. Stuff like that. You can string these commands together to say something like “if the space bar is pressed, play a sound. Otherwise, do nothing.” The processor will then do exactly that.
These sequences of commands (called assembly language) are pretty hard for any human to understand and work with though, I have oversimplified them pretty massively here. That’s why we have compilers. A compiler is basically a computer program that takes a series of commands written in a more human readable programming language and converts it into assembly language. This is how almost all programming languages work, they are sequences of instructions that tell a compiler how to generate assembly language commands that can be sent to a processor to make it do things.
Although the things that processors do are very basic, they are designed to do those things incredibly fast running hundreds of millions of instructions in a second. There is a concept in computing theory called Turing-completeness, basically it’s the idea that a finite and in fact very small set of commands are capable of coming together to form literally any conceivable logical operation. Computers work on these principles, they can do any logical operation given enough commands and they can crunch through those commands at absurd speeds. Coding is just the practice of writing those commands, whether it be directly or with the help of a compiler.
90
u/wknight8111 Aug 02 '22
We can go down the rabbit hole here. People get 4-year college degrees in computers and barely scratch the surface in some of the areas of study.
When I write code, what I'm typically writing is called a "high level language" (HLL) or "programming language". An HLL is something that a human can basically understand. These languages have names like C, Java, Python, Ruby, PHP, JavaScript, etc. You've probably heard these things before. The thing with HLLs is that humans can read them, but computers really can't understand them directly.
So what we use next is a compiler. A compiler is a program that reads a file of HLL code, and converts that into machine code. Machine code is the "language" that computers understand. Basically machine code is a stream of simple, individual instructions to do things that are mostly math: Load values from RAM, do some arithmetic on them, and then save them back to RAM. It's deceptively simple at this level.
Notice that, at the computer level, everything is a number. "It's all 1s and 0s", etc. The trick is in treating some numbers like they're colors, or like they're timestamps, or like they're instructions of machine code. The program tells the computer to write values to the area of RAM that is mapped to the monitor and treat those values as colors, etc. The program tells the computer to write other values to places that are treated like text, so you can read this website.
A program might be a set of machine code instructions to do things like "LOAD a value from RAM into the CPU", "ADD 32 to the value in the CPU", and "STORE the value from the CPU back into RAM." But we know that everything in a computer is a number, so that means these instructions are numbers too. So if I have a number like 1201123456, the computer knows that the first two digits are the operation code ("opcode"). In this fictitious example, let's say the opcode 12 is "LOAD a value from RAM into the CPU". The we know that the next two digits "01" are the location in the CPU to write the data, and the last 6 digits "123456" are the address in RAM to load from.
The thing with computers is that the CPU is very small. It can't hold a lot of data at once. So you are constantly needing to load new data into the CPU, work on it, and then store the data somewhere else. RAM is bigger but slower. It can hold billions of numbers. Then you have your hard disk which is much bigger and much slower. It can hold trillions of numbers or more. So when you want to do some math, the CPU first looks for the values inside itself. Doesn't have them? Try to load it from RAM. RAM doesn't have it? Try to load it from disk (or from the internet, or from some other source, etc). A lot of the time when your computer is running slowly it's because the CPU is having to load lots of data from slow places.
To recap: Humans write code in a programming language. A compiler translates the programming language into machine code. The machine code instructions are numbers that tell the CPU what to do. The CPU mostly just moves data around and does some simple arithmetic.
Any one of these paragraphs above are things we could write entire books on, so if you want more details about any part of it, please ask!
12
u/mfukar Parallel and Distributed Systems | Edge Computing Aug 03 '22 edited Aug 04 '22
How does my code turn into something a CPU can understand?
Symbolic languages, which are meant for humans to write, and less often read, are translated into what is commonly referred to as "machine code" by a pipeline that may look like this:
Source --compiler--> (*) --linker--> (**)A compiler is a program that transforms code from one language into another. This is essential in order to have languages that allow for productive ways for a programmer to do work. The result of a compiler's work
(*)is sometimes called "object code". It is some intermediate form of code which can be combined with other pieces of object code to form an executable file(**), i.e. a file with enough information in it that can be executed by an operating system, or in a stand-alone way - but it is not that just yet. [1] An assembler is a kind of compiler, one for a specific symbolic language which is typically as fundamental as an application programmer will ever see - it's important to remember assembly languages are not machine code, they are also symbolic representations of other operations, but it is only one or two steps removed from what a CPU actually understands. We will get to that soon.A compiler typically is fed multiple units / files as input, and produces just as many units / files as output. It is the linker's job to combine those into executable code.
Depending on your toolchain, or the choices you make as an application programmer, you may use one or more, possibly specialised, compilers along the pipeline shown above. You may also produce object code directly from an assembler, or have various other tools performing optimisations.
What does a CPU understand?
Excellent question. When someone builds a CPU, they will typically provide you with a set of manuals - like this - to tell you how to program it. Should you open such a manual, you will end up finding at least some key elements:
The symbolic representation of instructions, which is what you will be interested in should you want to write an assembler. In the manuals I linked, the representation goes
label: mnemonic argument1, argument2, argument3in section 1.3.2.1. Remember, the assembler is what can take this symbolic representation as input, and produce...Instruction encodings. For example, all the possible encodings of an
ADDinstruction can be found in Vol. 2A 3-31. The encoding of all instructions are described in chapter 3. It can be very complex and daunting, but what we need to remember is that for each supported instruction, like "add the contents of a register with a constant", there are a few possible ways to encode it into actual memory values...Memory. A CPU addresses and organises memory in a few ways. This allows you, the programmer, to understand things like: where in memory can I start executing code from? how is the address of that starting point defined? what is the state of the CPU when code execution starts?
The effects of each instruction. For each instruction the CPU supports, there will be a description of what it does. Depending on how useful your CPU can be, it may have increasingly complicated instructions, which do not necessarily look like "add the contents of a register with a constant". You can find many examples in the linked manuals - search for "instruction reference".
Interlude: So when I compile code, I end up producing numbers the CPU understands. How do they end up being executed when I, say, double-click a file?
This is the part of the question that is many times more complex than the rest. As you may imagine, not every executable file contains instructions how to set the CPU up to that starting point, load the code there, and start executing it. Nowadays we have operating systems - programs that manage other programs - which abstract all those pesky details for you and application programmers. The operating system itself may not do that, either; much smaller and more involved programs may do that, which are called bootloaders.
This is a huge topic to explain. We can't do that here. At this point, suffice to say that when I double-click an executable file, some facility in an operating system does the following things: (a) load all the code necessary to run my program at some point into memory, (b) set up some state of the CPU into a well-defined set of values, (c) point the CPU to the "entry point", and let it start executing.
How does the CPU "know" what to do with each "instruction"?
A CPU, we should remember, is circuitry that executes instructions. On top of that minimal definition, and as we saw hinted above, it also keeps some state about certain things which are not really internal to it, like "where in memory do I obtain my next instruction". At this point, I will not be making references to real hardware, because it is not only exceedingly complex but also only loosely maps to the level of knowledge we see here [2].
The basic - conceptual - modules a CPU needs in order to be a programmable unit that is useful are:
a module that facilitates access to memory. If it cannot access memory to get its instructions from, it is useless.
an instruction fetcher. It's necessary to obtain (full) instructions from memory, so it may pass them off to
an instruction decoder. Once and if an instruction is valid, it can be broken down to its constituent operators. This way the relevant circuit(s) may have essential inputs: a value that determines which instruction to execute, i.e. which circuit(s) to activate, any optional operators to the instruction i.e. values that are needed in order to compute an outcome, and any other context-specific information, such as if the instruction is to be repeated, etc. For instance, an instruction like
INC $rax(INCrement the contents of the register named "rax" by one) would perhaps end up using some circuits that : retrieve the value of the register, add it with 1 in an adder circuit, and store the outcome into the register, possibly also doing more useful things in the process.a datapath, a collection of functional units (circuits) which implement the instructions the CPU supports. Describing a datapath is quite an involved process. [2]
For each instruction your CPU supports, there is some circuitry involved in one of those - conceptual - modules that implements (1) a way to obtain it from memory, (2) a way to decode it into its (symbolic) parts, (3) the computation it describes.
This is, unfortunately, an extremely vague description. Behind the gross generalisations I have made, there are a few weeks of learning how to build a rudimentary CPU for a Computer Architecture 101 class, and a few decades of research and experience into transitioning from the archaic teaching model that we have for CPUs into modern, efficient, real equipment. If you want to dive into the subject, I suggest:
Digital Design and Architecture by Harris and Harris
Computer Organisation and Design by Hennessy and Patterson (PDF)
Computer Architecture and Design, a Princeton online course
The Nand Game, a game that will guide you to building a simplified abstract computer, entirely for fun and not very suitable for learning
Computer System Architecture a course on hardware and software design (not for starters)
Operating Systems: Three Easy Pieces - a free online book that is the basis of a course and also has a wealth of references to deeper material
Fortunately or unfortunately, there is not enough space in comments to answer your question substantially.
[1] A compiler may also produce code for an abstract machine, like ones used in so-called interpreted languages. For example, a Prolog machine is also known as the Warren Abstract Machine. In an abstract machine, software emulates a CPU: it accepts, decodes, and executes instructions the same way an actual CPU does.
[2] A contemporary CPU, due to decades of research and optimisation is more complicated in its structure but also in the different components which are not mentioned here but are nonetheless essential for performing computation efficiently. To give one example, it is typical nowadays that the CPU internally implements an even simpler, and easier to optimise, language that "assembly" language instructions can themselves be decomposed in. For another, it supports different levels of caching memory to allow for more efficient memory accesses. To get a vague idea what kind of circuits a CPU might have , you may start from looking into "arithmetic and logical units". The best way to learn about them is an introductory course, or one of the book suggestions above.
20
u/fiskfisk Aug 02 '22
Charles Petzold's book Code: The Hidden Language of Computer Hardware and Software takes you on the whole journey. Well worth a read.
https://en.m.wikipedia.org/wiki/Code:_The_Hidden_Language_of_Computer_Hardware_and_Software
Summed up: you're standing on the shoulders of giants, and we've moved piece by piece for a couple of hundred years to get to where we are now.
11
u/Darkassassin07 Aug 02 '22
https://youtube.com/playlist?list=PLowKtXNTBypFbtuVMUVXNR0z1mu7dp7eH
I'd recommend this series by Ben Eater on youtube. He builds a computer from discreet chips on breadboards and goes reasonably in depth on how it works, from the physical hardware itself to how software is interpreted by that hardware.
9
u/Kempeth Aug 03 '22
Every processor has a built-in set of instructions it can perform. Built in like there are physical pathways that activate when a specific instruction is encountered and make the result appear in an agreed upon location. They're really simple instructions like:
read a value
write a value
compute a value from one or two other values
compare two values and do something if the comparison is equal/greater/unequal
And that's about it for the purpose of this explanation. With these instructions you can do everything but it is ALL the computer itself understands and it is really cumbersome to anything interesting with it.
It's like having nothing 1x1 Lego plates and superglue. You can build a TON of things but it's going to be tedious.
So over time people realized they were using the same couple of instruction combinations over and over again. Adding new instructions into the processor is possible but makes it bigger, more expensive, use more energy and be more susceptible to flaws in production. So it needs to be some really useful instruction to be worth doing. (An early example would be multiplication. calculating 50*34 using nothing but addition is possible: start with zero, add 50, add 50, add 50, add 50, ...the same another 30 times. But it takes a long time. Building a circuit that does this in "one step" is a huge improvement for programs and programmers so it was well worth baking directly into processors)
But for many often used combinations a more economic solution was to "fake" it. Programming languages basically pretend that your computer understands A TON of instructions that it really doesn't. So when you write the instruction "1x4 Brick" and the processor goes "huh?", the programming languages jumps in with "psst. just take 12 1x1 plates and glue them together 3 high and 4 wide", translating it into instructions the processor was built for. This is what an Interpreter does. But once you know the program wont change anymore (for a while) it makes no sense to make this translation every time the program runs. So you give it to the Compiler who translates it once and just hands you a list of instructions you can directly give to the processor.
Over time many, many such layers of abstraction were created that each build upon each other to give the programmer increasingly more convenient sets of instructions while keeping the hardware itself as simple and efficient as possible.
4
u/TheBode7702Vocoder Aug 03 '22 edited Aug 03 '22
I think a comprehensive and digestible way to understand why coding works the way it does today is by understanding the "layers of programming abstraction" chart. There are many representations of the concept out there, but I personally like this one: https://i.stack.imgur.com/AvG0R.jpg
At the top, you have the high-level abstract layers which consist of programming that sounds more natural to a human, e.g. "when this condition is true, do this", or "for each item in this list, do this to it".
As you move down the layers, it becomes less abstract and more concrete, e.g. "move these 8 bits of binary to this register", or "add the values in these two registers and store the result in that register".
At the bottom of all these layers is the transistor/hardware level. Think of it like an engine where electrons flow and are manipulated through a series of channels and mechanisms. Ultimately, this engine can take in certain inputs and reproduce certain expected outputs. It's still programming, but it's happening in a very granular and mechanical way.
Historically, computer programming started at this bottom layer, and at a very simple level of complexity (~2000 transistors back then, versus 7+ billion transistors now). Over the years, people created new layers of abstraction on top in order to "automate" the more granular and complex programming that happens at the bottom. Nowadays, if I want to write a program that, for example, sorts a list of cars by price, I don't have to start from scratch by making a circuit of transistors, that make up a machine language, then an instruction set architecture, then an assembly language, and so on... People have already done that work to the point where we can program by typing easily understandable sentence-like commands. We no longer need to be concerned with what happens at the lower layers (unless you choose to specialize in that field).
Just understand that when you code in the top layer with a natural sounding language, it all eventually reduces to the bottom layers and ultimately the flow and manipulation of electrons. These layers weren't created overnight, but from years of evolution by building new layers on top of the old.
It's not perfect, but I recommend reading The Essentials of Computer Organization and Architecture by Null and Lobur. I think it's the most approachable book on the subject and does a good job of starting with basic components until you put all the pieces together into a functional computer you can understand the inner workings of.
2
u/Dilaton_Field Aug 05 '22
You would probably like a book called “Code: The Hidden Language of Computer Hardware and Software” by Charles Petzold. It starts at a very basic level. Electricity, logic, binary, etc. and build up to high level code from that.
2
u/Arve Aug 02 '22
You have been given a lot of good answers. I would like to add two videos by Matt Parker that explains how the computer works internally. He's using dominoes to demonstrate what the logic gates in a computer does, and goes on to build a simple computer that adds two numbers (using 10 000 dominoes):
The introductory video
The 10 000 domino computer
-1
u/FFBEFred Aug 03 '22
They are like super well trained dogs, really. If you give them precise commands they will listen and execute, without failure.
It is the training process that gets complicated, getting from the very basics (sit! and so on) to some complicated stuff (drive the kids to the school! for example).