Storage 26-Jul-16 Parts of a computer For purposes of this talk, we will assume three main parts to a computer Main memory, once upon a time called core, but these days called RAM (Random Access Memory) Peripheral memory, these days called disks (even when they aren’t) or drives RAM consists of a very long sequence of bits, organized into bytes (8-bit units) or words (longer units) Peripheral memory consists of a very, very long sequence of bits, organized into pages of words or bytes Peripheral memory is thousands of times slower than RAM The CPU (Central Processing Unit), which manipulates these bits, and moves them back and forth between main memory and peripheral memory 2 It’s all bits Everything in a computer is represented by a sequence of bits—integers, floating point numbers, characters, and, most importantly, instructions Bits are the ultimate flexible representation—at least until we have working quantum computers, which use qubits (quantum bits) Modern languages use strong typing to prevent you from accidentally treating a floating point number as a boolean, or a string as an integer A weakly typed language provides some protection, but there are ways around it But it wasn’t always this way... 3 Storage is storage At one time, words representing machine instructions and words representing data could be intermixed Strong typing was a thing of the future It was the programmer’s responsibility to avoid executing data, or doing arithmetic on instructions Both of these things could be done, either accidentally or deliberately Machine instructions are just a sequence of bits They can be manipulated like any other sequence of bits Hence, programmers could change any instruction into any other instruction (of the same size), or rewrite whole blocks of instructions A self-modifying program is one that changes its own instructions 4 Self-modifying programs Once upon a time, self-modifying programs were thought of as a good thing Just think of how flexible your programs could be! ...yes, and smoking was once considered good for your health The usual way to step through an array was by adding one to the address part of a load or store instruction You could write some really clever self-modifying programs But, as the poet Piet Hein says: Here’s a good rule of thumb: Too clever is dumb. 5 Preparation for next example In the next example, we will talk about how a higher-level language might be translated into assembly languages Here are some of the assembly instructions we will use: The load instruction copies a value from a memory location into a special register called the accumulator The enter instruction puts a given value into the accumulator Example: enter 53 puts 53 itself into the accumulator All arithmetic is done in the accumulator Example: load 53 gets whatever is in location 53 and puts it into the accumulator Example: add 53 adds the contents of location 53 to the accumulator The store instruction copies a value from the accumulator into memory Example: store 53 puts whatever is in the accumulator into location 53 6 Procedure calls Consider the following: a = add(b, c); ... function add(x, y) { return x + y; } Here’s how it might have been translated to assembly language in the old days (red values are filled in as the program runs): 42 [ 0] // a 43 [ 10] // b 44 [ 15] // c 20 [load from 43] // addr of b 21 [store in 71] 22 [load from 44] // addr of c 23 [store in 72] 24 [enter 27] // the return addr 25 [store in addr part of 70] 26 [jump to 73] 27 [store in 42] // addr of a 70 [jump to 27] // gets return addr 71 [ 10] // will receive b 72 [ 15] // will receive c 73 [load value at addr 71] 74 [add value at addr 72] 75 [jump to 70] 7 Problems with the previous code In this example, storage was static—you always knew where everything was (and it didn’t move around) If you called a function, you told it where to return to, by storing the return address in the function itself You stored the parameter values in the function itself This worked fine until recursion was invented Recursion requires: Hence, you could call the function from (almost) anywhere, and it would find its way back Multiple return addresses Multiple copies of parameters and local variables In other words, recursion requires dynamic storage 8 The end of an era What really killed off self-modifying programs was the advent of timesharing computers Multiple users, or at least multiple programs, could share the computer, taking turns But there isn’t always enough main memory to satisfy everybody When one program is running, another program (or parts of it) may need to be copied to disk, and brought back in again later This is really, really slow If only the data changed, not the program, we wouldn’t have to save the program (which is often the largest part) over and over and over... Besides, with the new emphasis on understandable programs, self-modifying programs were turning out to be a Really Bad Idea Besides, think about what a security nightmare self-modifying programs could be! 9 An aside—compilers and loaders Although self-modifying code is a bad idea, it is still necessary for computers to be able to create and modify machine instructions This is what a compiler does—it creates machine instructions A loader takes a compiled program and puts it somewhere in a computer memory It can’t always put it in the same place, so it has to be able to modify the addresses in the instructions Still, compilers and loaders don’t modify themselves 10 Static and dynamic storage In the beginning, storage was static—you declared your variables at the beginning of the program, and that was all you got A procedure or function with, say, three parameters, got three words in which to store them The parameters went in a fixed, known location in memory, assigned to them by the compiler Recursion had not yet been invented The programming language Algol 60 introduced recursive functions and procedures Parameters went onto a stack Hence, parameters were dynamically assigned to memory locations, not by the compiler, but by the running program itself Storage was dynamically allocated and deallocated as needed 11 Stacks Stacks obey a simple regimen—last in, first out (LIFO) When you enter a function or procedure or method, storage is allocated for you on the stack When you leave, the storage is released In Java, this is even more fine-grained—storage is allocated and deallocated for individual blocks, and even for for statements Since this is so well-defined, your compiler writes the code to do it for you But it’s still dynamic—done by your running program Since virtually every language supports recursion these days (and all the popular languages do), computers typically provide machine-language instructions to simplify stack operations 12 Heaps Stacks are great, but they have their limitations Suppose you want to write a method to read in an array You enter the method, and declare the array, thus dynamically allocating space for it You read values into the array You return from the method and POOF! your array is gone You need something more flexible—something where you have control over allocation and deallocation The invention that allows this (which came somewhat later than the stack, I’m not sure when) is the heap You explicitly get storage via malloc (C) or new (Java) The storage remains until you are done with it 13 Stacks vs. heaps Stack allocation and deallocation is very regular Heap allocation and deallocation is unpredictable Stack allocation and deallocation is handled by the compiler Heap allocation is at the whim of the programmer Heap deallocation may also be up to the programmer (C, C++) or by the programming language system (Java) Values on stacks are typically small and uniform in size In Java, arrays and objects don’t go in the stack—references to them do Values on the heap can be any size Stacks are tightly packed, with no wasted space Deallocation can leave gaps in the heap 14 Implementing a heap A heap is a single large area of storage When the program requests a block of storage, it is given a pointer (reference) to some part of this storage that is not already in use The task of the heap routines is to keep track of which parts of the heap are available and which are in use To do this, the heap routines create a linked list of blocks of varying sizes Every block, whether available or in use, contains header information about the block We will describe a simple implementation in which each block header contains two items of information: A pointer to the next block, and user gets The size of this block from here pointer to next size of block User data (an Object) on down 15 Anatomy of a block Here is our simple block: user gets N words from here (ptr) to end of block ptr-2 ptr-1 ptr ptr+1 ptr+2 : : pointer to next size of block User data (an Object) ptr+N-1 Java Objects hold more information than this (for example, the class of the object) Notice that our implementation will return a pointer to the first word available to the user Data with negative offsets are header data ptr-1 contains the size of this block, including header information ptr-2 will be used to construct a free space list of available blocks 16 The heap, I free 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 next = 0 size = 20 Initially, the user has no blocks, and the free space list consists of a single block In our implementation, we will allocate space from the end of the block To begin, let’s assume that the user asks for a block of two words 17 The heap, II 0 next = 0 1 size = 16 free 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 next = 0 given to 17 size = 4 user 18 //////////// 19 //////////// The user has asked for a block of size 2 The “free” block is reduced in size from 20 to 16 (two words asked for by the user, plus two for a new header) The new block has size 4 and the next field is not used Next, assume the user asks for a block of three words 18 The heap, III 0 next = 0 1 size = 11 free 2 3 4 5 6 7 8 9 10 11 next = 0 given to 12 size = 5 user 13 //////////// 14 //////////// 15 //////////// 16 next = 0 17 size = 4 18 //////////// 19 //////////// The user has asked for a block of size 3 The “free” block is reduced in size from 16 to 11 (three words asked for by the user, plus two for a new header) The new block has size 5 and the next field is not used Next, assume the user asks for a block of just one word 19 The heap, IV 0 1 free 2 3 4 5 6 7 8 given to 9 user 10 11 12 13 14 15 16 17 18 19 next = 0 size = 8 next = 0 size = 3 //////////// next = 0 size = 5 //////////// //////////// //////////// next = 0 size = 4 //////////// //////////// The user has asked for a block of size 1 The “free” block is reduced in size from 11 to 8 (one word for the user, plus two for a new header) The new block has size 3 and the next field is not used Next, the user releases the second block (at 13) 20 The heap, V free 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 next = 0 size = 8 next = 0 size = 3 //////////// next = 2 size = 5 next = 0 size = 4 //////////// //////////// The user has released the block of size 5 The freed block is added to the front of the free space list: Its next field is set to the old value of free free is set to point to this block Next, the user requests a block of size 4 The first block on the free list isn’t large enough, so we have to go to the next free block 21 The heap, VI 0 1 2 given to 3 user 4 5 6 7 8 9 10 11 12 free 13 14 15 16 17 18 19 next = 0 size = 2 next = 0 size = 6 //////////// //////////// //////////// //////////// next = 0 size = 3 //////////// next = 2 size = 5 next = 0 size = 4 //////////// //////////// The user requests a block of size 3 The size of the first free block is now 3, and its next field does not change The user gets a pointer to the new block Now the user releases the smallest block (at 10) Again, this will be added to the beginning of the free space list 22 The heap, VII free 0 next = 0 1 size = 2 2 next = 0 3 size = 6 4 //////////// 5 //////////// 6 //////////// 7 //////////// 8 next = 13 9 size = 3 10 11 next = 2 12 size = 5 13 14 15 16 next = 0 17 size = 4 18 //////////// 19 //////////// The user releases the smallest block (at 10) The freed block is added to the front of the free space list: Its next field is set to the old value of free free is set to point to this block Now the user requests a block of size 4 Currently, we cannot satisfy this request We have enough space, but no single block is large enough However, free blocks 10 and 13 are adjacent to each other We can coalesce blocks 10 and 13 23 The heap, VIII free 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 next = 0 size = 2 next = 0 size = 6 //////////// //////////// //////////// //////////// next = 2 size = 8 Blocks at 10 and 13 have now been coalesced The size of the new block is the sum of the sizes of the old blocks We had to adjust the links Now we can give the user a block of size 4 next = 0 size = 4 //////////// //////////// 24 Pointers Allocating storage from the heap is easy In Java, you request storage from the heap with new; there is no other way to get storage on the heap The implementation is identical; the difference is that there are more operations on pointers than on references C and C++ provide operations on pointers All Objects are on the heap In C and C++ you get a pointer to the new storage; in Java you get a reference Person p = new Person ( ); C and C++ let you do arithmetic on pointers, for example, p++; Pointers are pervasive in C and C++; you can't avoid them 25 Advantages/disadvantages Pointers give you: References give you: Greater flexibility and (maybe) convenience A much more complicated syntax More ways to create hard-to-find errors Serious security holes Less flexibility (no pointer arithmetic) Simpler syntax, more like that of other variables Much safer programs with fewer mysterious bugs Pointer arithmetic is inherently unsafe You can accidentally point to the wrong thing You cannot be sure of the type of the thing you are pointing to 26 Deallocation There are two potential errors when de-allocating (freeing) storage yourself: De-allocating too soon, so that you have dangling references (pointers to storage that has been freed and possibly reused) A dangling reference is not a null link—it points to something (you just don’t know what) Forgetting to de-allocate, so that unused storage accumulates and you have a memory leak If you have to de-allocate storage yourself, a good strategy is to keep track of which function or method “owns” the storage The function that owns the storage is responsible for de-allocating it Ownership can be transferred to another function or method You just need a clearly defined policy for determining ownership In practice, this is easier said than done 27 Discipline Most C/C++ advocates say: It's just a matter of being disciplined I'm disciplined, even if other people aren't Besides, there are good tools for finding memory problems However: Virtually all large C/C++ programs have memory problems 28 Garbage collection Garbage is storage that has been allocated but is not longer available to the program It's easy to create garbage: A garbage collector automatically finds and de-allocates garbage Allocate some storage and save the pointer to it in a variable Assign a different value to that variable This is far safer (and more convenient) than having the programmer do it Dangling references cannot happen Memory leaks, while not impossible, are pretty unlikely Practically every modern language, not including C++, uses a garbage collector 29 Garbage collection algorithms There are two well-known algorithms (and several not so well known ones) for doing garbage collection: Reference counting Mark and sweep 30 Reference counting When a block of storage is allocated, it includes header data that contains an integer reference count The reference count keeps track of how many references the program has to that block Any assignment to a reference variable modifies reference counts If the variable previously referenced an object (was not null), the reference count of that object is decremented If the new value is an object (not null), the reference count for the new object is incremented When a reference count reaches zero, the storage can immediately be garbage collected For this to work, the reference count has to be at a known displacement from the reference (pointer) If arbitrary pointer arithmetic is allowed, this condition cannot be guaranteed 31 Problems with reference counting If object A points to object B, and object B points to object A, then each is referenced, even if nothing else in the program references either one This fools the garbage collector, which doesn't collect either object A or object B Thus, reference counting is imperfect and unreliable; memory leaks still happen However, reference counting is a simple technique and is occasionally used 32 Mark and sweep When memory runs low, languages that use mark-andsweep temporarily pause the program and run the garbage collector The collector marks every block It then does an exhaustive search, starting from every reference variable in the program, and unmarks all the storage it can reach When done, every block that is still marked must not be accessible from the program; it is garbage that can be freed In order for this technique to work, It must be possible to find every block (so they are in a linked list) It must be possible to find and follow every reference The mark has to be at a known displacement from the reference Again, this is not compatible with arbitrary pointer arithmetic 33 Problems with mark and sweep Mark-and-sweep is a complex algorithm that takes substantial time Unlike reference counting, it must be done all at once— nothing else can be going on The program stops responding during garbage collection This is unsuitable for many real-time applications 34 Garbage collection in Java Java uses mark-and-sweep Mark-and-sweep is highly reliable, but may cause unexpected slowdowns You can ask Java to do garbage collection at a time you feel is more appropriate The call is System.gc(); But not all implementations respect your request This problem is known and is being worked on There is also a “Real-time Specification for Java” 35 No garbage collection in C or C++ C and C++ do not have garbage collection—it is up to the programmer to explicitly free storage when it is no longer needed by the program C and C++ have pointer arithmetic, which means that pointers might point anywhere There is no way to do reference counting if the programming language does not have strict control over pointers There is no way to do mark-and-sweep if the programming language does not have strict control over pointers Pointer arithmetic and garbage collection are incompatible--it is essentially impossible to have both 36 The End 37