Introduction To Software Development
Introduction To Software Development
Introduction To Software Development
Introduction to Software
Development
Preface i
0.1 About these Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
0.2 Disclaimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
0.3 Example Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii
0.4 Code Snippets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ii
0.5 Glossaries and Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii
5 Wrap Up 94
5.1 Warning: The Expert Beginner . . . . . . . . . . . . . . . . . . . . . . 94
5.2 Where to go From Here . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Glossary - General Programming . . . . . . . . . . . . . . . . . . . . . . . . 96
The notes may redistributed freely with attribution, but may not be used for commercial
purposes nor altered or modified. The Angry Penguin and other reproduced material,
is clearly marked in the text and is not included in this declaration.
The notes were typeset in LATEXby H Ratcliffe.
Errors can be reported to rse@warwick.ac.uk
0.2 Disclaimer
These talks are going to take a very practical stance and will not always agree with the
beginner textbook theories you may have encountered. We’re working from experience
of actually writing scientific code, and we’ll always try to note where something is a
practical shortcut or sidestep. However perhaps the crucial thing to remember is rules
are only useful until they’re not, and often advice is given with an understanding that
you will come to understand exactly why it was so formed, at which point you have the
necessary discretion to know when to ignore it.
If you have ever read any of Terry Pratchett’s Science of Discworld, for example,
you may have encountered “lies to children”,
i
ii PREFACE
... a statement that is false, but which nevertheless leads the child’s
mind towards a more accurate explanation, one that the child will only be
able to appreciate if it has been primed with the lie
When statements fall into this category, they are currently useful, but one day will
cease to be so. This is important to remember, and doubly important to remember if
you find yourself passing on your new wisdom.
In general, we will only say that you mustn’t do something, if we think that in
all likelihood it will never be the correct thing to do. It may break standards or we
see no circumstance where it would win in the cost-benefit stakes. (Although we may
occasionally be wrong on that.) Shouldn’t encompasses all of those currently, or
generally, useful generalisations. If you find yourself going against a recommendation
like that, think carefully before proceeding. Must and should are used similarly. And
finally, if something you’re trying to do seems painfully difficult or awkward, consider
the possibility that there is a better way to do it.
After reading these notes, or attending our workshops, you should be in a good
position to understand more detailed material, and to know what terms and phrases
to search for to solve problems you encounter. Software development is a wide field
with many schools of thought, often with incompatible constraints and priorities. As
researchers, your priority is research. You code needs to work, and you need
to know it works. Anything that does not enhance that goal is decoration.
9 IF ( i < 1 0 ) THEN
10
11 ENDIF
C
1 int i ;
2 double r ;
3 float [ 1 0 ] arr ;
4 b o o l l ; // C99 o r C++ o n l y
5 f o r ( i = 0 ; i < 1 0 ; i ++){
6 p r i n t f ( ’%d\n ’ , i ) ; // P r i n t numbers 0 t o 100
7 }
8 i f ( i < 10) {
9 }
Python
1 f o r i in range ( 0 , 1 0 0 ) :
2 print i #P r i n t numbers 0 t o 100
3 i f i < 10:
4 pass
Sometimes we also show snippets of commands for shell, make, or git. These often
contain parts which you should substitute with the relevant text you want to use.
These are marked with {}, such as
1 g i t branch {name}
Introduction to Software
Development
Before sitting down at your editor and actually writing code, you should always
spend at least a little time on actually designing your program, no matter how simple.
In time you will develop a preferred method of doing this, but how doesn’t matter
nearly as much as having some kind of plan. How detailed your plan needs to be
depends on a few simple factors, and a lot of complicated ones. If your code involves
any sensitive data, you really must read up on relevant regulations and
protocol. This includes:
• Personal data
Otherwise, your design can save you time, heartache and hair, but it needn’t be carefully
documented.
1
2 CHAPTER 1. INTRODUCTION TO SOFTWARE DEVELOPMENT
and various other aids to structuring your code. At this stage, there are a few general
rules you should follow, many more that may be useful, and a very few things you
must do.
Essential Rules
• Create a working code - it sounds silly, but the absolute key thing is that you
end up with some code that works, does what was intended, and is reasonably
dependable, i.e. wont break unexpectedly or in hard to detect ways.
• Follow your chosen language standard - we will discuss this in more detail
below, but the standards dictate what is valid code and a valid program. For
example, in a valid Fortran program, all variable definitions must go at the top of
a function. In a valid C program, all functions must have been defined before they
are first used (either completely or via a “prototype” giving function arguments
and return type)
• Use some sort of versioning - people joke about code iterations labelled
“my function fixed mike January.c” and that is clearly silly, but one can do silly
things with version control too. The core idea is to have some sort of record of
when changes were made (and by whom). If you choose to do daily dated back-
ups, and have the discipline to do it, that is fine. On the other hand, systems like
git have built in abilities to compare, search and assign blame. See Sec 4.3 for
more.
• Validate your code enough - checking and testing is vital, but what “enough”
means varies. For a one-off small script, you might just run an example case; for
large projects you will want to consider formal testing and validation
Useful Guidelines
• Divide into reasonably sized functions - in general, functions should do one
thing, but in practice that one thing can become complicated. For example,
writing data to a file is probably a single action, but need not be short. You may
hear absolute rules like functions should be 10 lines or less, but you will likely
notice that this is rubbish, and can add tangle. There is no line limit, but it is
easier to write, and far easier to debug a function if you are not having to think
about twenty things at once.
• Lay out equations etc to help a human - all formatting is gone once your code is
compiled, or hits the interpreter, so use it to your advantage. You can use line
breaks to split terms of a calculation into logical groups, align groups horizontally,
use spacing to group and divide terms. You may also wish to keep equations
as they appear in books or papers for now, as they can be rearranged later.
Alternatively, you can put human-readable equations in comments.
• Document as you go - we’ll discuss documenting a bit more, but in general do it
as you go, but never before a function’s purpose has been worked out. Orphaned
comments can be very confusing.
1.1. BASIC SOFTWARE DESIGN 3
Finally, there are many things that are good guidelines in general programming that
are far more subjective for scientific code. A few of these are below:
• Global variables - One often encounters statements like “global variables are evil”.
There is some truth in the reasoning - functions which modify a global have “side
effects” elsewhere in your code. However, consider a program calculating (some
physical field quantity). Something that you seem to be passing to “nearly every”
function is probably an actual global state, and if it is large you will not want to
copy it around. A carefully named and documented global variable is not a bad
solution here.
• You Aren’t Going to Need It (YAGNI) - In general, create the simplest program
to solve your problem. BUT do consider slightly what you’ll need from it next
week or you’ll find yourself writing non-extensible code and doing a lot of work.
Don’t add every bell and whistle though.
• Do not optimise (at this stage) - similarly, until your code works, don’t bother
with (most) optimisation. BUT do avoid making choices that can never be fast
enough.
• Don’t repeat yourself (DRY) - this is a good general principle, BUT do not take
it to extremes. In general it is better not to copy-and-paste code and then make
small edits, but it is easy to find examples where things are subtly different and
trying to write one piece to do them all makes it more complex and/or badly
performing.
• “There’s only one way to do it” from the Zen of Python - this idea sounds nice, but
we think it breaks down here, and is something no other language even tries to do.
There are many ways to solve most problems, and for the sort of code you’ll be
writing, your concerns may be slightly unusual and different to the norm. Getting
things to work, and to perform adequately is more important than any points of
philosophy.
Before you start coding in earnest, you will want to have some sort of
plan. This need not be written down, nor be in any special form. For example, for a
very small script you may need nothing more than your idea of what it should do, and
a first guess as to how. For a large piece of code that will take you weeks or months,
you probably do want some sort of formal plan.
You may find it helpful to write a flow-chart for your program, usually focusing on
the movement of data. Plan out the general sections, for example user interactions,
file I/O, your core algorithm. If you are planning complicated programs or want a
4 CHAPTER 1. INTRODUCTION TO SOFTWARE DEVELOPMENT
standard format to share with others, you may want to consider Unified Modeling
Language (UML). This is rather overkill for most purposes, but can be useful for formal
diagrams.
Pay particular attention to limitations and ordering, such as
3. Which sections are likely to be the crucial ones for testing and optimising?
The section quote above is worth keeping in mind. When creating your plan, the
final result is not the only goal. For starters, once you actually try to implement your
plan there is a fair chance you will have to change it. Your goal is more to survey the
field, find out the limitations and problems, work out how much work this is going to
be etc.
9 END FUNCTION
10
11 BEGIN MAIN
12
13 INTEGER x = 2
14
15 PRINT add ( i n c r e m e n t ( x ) , x )
16
17 END MAIN
and try to work out the value printed. When the add function is called, the compiler
packs up the arguments and passes them (by value). If the function’s arguments are
worked out from left to right (and pass(ed) by reference), then you expect a value of 6
(incremented x plus incremented x), whereas if they go from right to left or are pass(ed)
by value, you expect 5 (x plus incremented x). In C/C++ for example the order is not
guaranteed, even though your compiler may always give you the same result.1
One of the few rules in this guide is do not write code that violates standards
or has undefined behaviour. Even if it seems harmless now, it can backfire badly.
In C the joke goes that undefined behaviour can crash (the best result, as at least
you’ll know), delete your hard drive contents (unlikely), or cause the implosion of the
entire universe (very unlikely2 ). The likely worst case scenario is code which doesn’t
quite work right. Perhaps it works fine except when the same function is called 3
times in succession. Perhaps a particular value just gives the wrong answer. Perhaps it
crashes one time in 10000. Bugs which you can’t reliably reproduce, sometimes called
Heisenbugs3 , are a nightmare
each pile has only one item (assuming no duplicate names).5 It is not easy to see, but
this has a time requirement proportional to only n ln n and for large n this is rather
quicker.
As a second example, imagine you have your alphabetized essays and now wish to
locate a particular student. You might start at the beginning and look until you find
them, which in general will require you look at n/2 names. A common alternative is to
pick the middle of the list and decide if your target name is before or after that name.
You then take the relevant half of the pile, and take the middle element and again check
for before or after. This requires only ln n examinations, which grows very slowly as
n gets bigger. This is an example of an approach called bisection. Binary bisection
has uses in all sorts of areas, so make sure you understand how it works.
Searching and sorting are prime examples of well developed, widely available, best
practice solutions to a problem, where failing to research can make your code much
slower than it needs to be. Slowness is sometimes acceptable, but it limits your code
to smaller problems than you may one day wish to work on.
A more serious example involves money calculations. You might initially try storing
floating-point, or real, numbers for currency amounts. However, you can quickly run
into rounding issues because of how floats work. An easy first “fix” is to instead store
integers for number of pennies. As long as your total amounts are never too large this
is OK, until you attempt to deal with percentage calculations. An item costing 4.99
with a 10% discount could reasonably cost either 4.50 (10% giving 49.9p, truncated to
49 p before subtraction) or 4.49 (49.9p, rounded). Several films have people collecting
all those fractions of pennies for themselves. This problem is particularly obvious with
compound interest, where any rounding errors can grow. Banks have strict protocols
on how these calculations must be done, and if protocol matters you must find
and follow the proper guidelines.
There are many very good books on algorithms in general, and on algorithms and
techniques for all sorts of fields. Here we just summarise some points to keep in mind
when selecting your methods, in the form of inspirational quotes:
• As simple as possible, but no simpler. Don’t choose complex methods just for
fun, as they do tend to be trickier to understand, implement and test. But don’t
oversimplify either.
• Don’t re-invent the wheel. Take advantage of prior art. However, sometimes
the wheel is for a bicycle and you’re designing a jumbo-jet, so it won’t fit. The
converse isn’t much better - jumbo-jet wheels are far more complex and costly to
maintain than a suitable bicycle wheel.
• Don’t pour good money after bad. Sometimes you will make a mis-step and spend
time on unproductive routes. Be willing to put that work aside and try again.
Don’t throw it away though - it may be useful another time.
5
This is basically a radix or bucket sort although in practise you might do it more like a merge sort.
1.1. BASIC SOFTWARE DESIGN 7
• Better the devil you know. If you know a technique that will work, but may,
for example, be a bit more computationally demanding, it may still be a better
choice than trying something completely unfamiliar.
An often overlooked consideration is that of when you simply should NOT write
code. This is fairly unusual, although it should probably be considered more often that
it is in practice. Note first the trivial example - when you need only a simple, single use
script for a task. It is probably a waste of effort to generalise beyond your immediate
needs. Otherwise, consider not creating something yourself when:
• A tool exists that can do your task well enough. For example, data conversion
where a simple Excel import will suffice, or web-page creation where you might
be better using a WSIWYG editor rather than writing HTML.
• When the effort would not be balanced by the reward. Sometimes you could
create something, BUT it would take time or effort that would not be rewarded,
and you would be better taking a different research tack.
• When you lack the expertise. Again, no matter how tempting, if your code will
require research-level computer science or algorithm knowledge, you may be better
off adapting your research problem to the tools available, as you may not even
realise the errors you are making.
11 lines of code designed to pad a string to a given length. Thousands of projects fell
over, because this crucial dependency was now missing. The package was subsequently
restored; a few simple commands got projects working again, and the system was
changed so packages can never be removed. Using libraries for such “trivial” function
can be dangerous, as you increase complexity for little gain.
Remember that every library you use is another thing your code depends on, another
thing your users (or you) have to install, and another source of variations outside your
control. In one code we work with, there are several released versions of a particular
library (MPI) which simply do not work. We are able to check the version and inform
the user before failing gracefully in these cases, but as the number of libraries grows
this becomes impossible. Imagine you have a code that uses 2 libraries, each of which
has 3 common versions. You now have 9 different possible environments that your code
may use, and this can get difficult to debug if a user reports a problem. Some versions
of some of your libraries may not even be mutually compatible. The moral here is
NOT that you should not use libraries, but to be selective:
Absorb what is useful, discard what is useless and add what is specifically
your own – Bruce Lee
Often you may find that your projects grow to fit the time available for them. This is
not uncommon, and is the origin of the section quote. Sometimes, though, you will need
to get a good estimate of how long you will need long before you start coding. This is
one of the trickiest parts of development, and can take years to perfect. Formal systems
based on Function Point analysis6 abound. These use a proxies to try and estimate the
size and complexity of a project based on known factors and previous experience. A very
simplified application, aimed particularly towards academic code, is given in our esti-
mators at https://warwick.ac.uk/research/rtp/sc/rse/project_estimator.pdf
and https://warwick.ac.uk/research/rtp/sc/rse/project_estimatorwexample.
pdf (including a worked example). This tries to quantify size and complexity based on
inputs, outputs and libraries. If you have a recent project handy, try the sheet out to
calibrate your estimates.
Contrast this with “procedural” programming, where your code is a series of functions
which are called directly. Finally, there are the more specialised “functional” style,
where one abstracts the idea of the program’s “state” and favours “recursion”; and
“declarative” programming, where one states what should be done, and the engine
(often a database system) decides how to do it.
The other meaning for programming model relates to the design principles. The first
formal software design methodology was what is now called waterfall development. Here
design proceeds in stages, with each stage being fully and completely determined before
the next step is started. As we have mentioned at least once, some types of program
have to work correctly because they handle people’s data, money or safety. These
often benefit from waterfall design: detailed program specifications are written up and
confirmed before any code is created; code is then tested against these specifications;
and in theory nothing is released which does not meet the standards set. The general
alternative school of methods is based around agile development. These come from
industry, where the customer tends to change the specifications at will, request new
features close to deadlines, and refuse to clarify their needs. In response to this, agile
methods only pin-down what will be done in short chunks, often a week at a time,
and rely on regularly releasing working but incomplete code. This can work well in
academic practice, since it minimises the time spent on formal design, but be careful
to pin down enough details to make your code useful.
hard coded name. You can extend the program later to take a filename. Later again
you may add an actual file browser to choose the file. Put another way, the MUU is the
target to hit for your program or addition to be a success. That file-browser is no use if
it doesn’t actually work, and you’d mostly never want to share code which is broken.7
For scientific code, the MUU is often first the simplest code that will let you produce
papers, and then it is any increment which allows you to produce a new paper. From
another perspective it is whatever will satisfy your funder. Note that MUU is not
meant as a perjorative, unlike the related Minimum Publishable Unit. Incremental
development is powerful and lets you get work done even while you develop your code
further.
It is always a good idea to write a simple program with a new tool, before
trying to integrate it into your actual code. Even experienced programmers find
it difficult to understand complex tools within complex programs. If one learns to play
an instrument, the first steps are simply learning to handle it and to play single notes
or chords, before combining them in sequences.
Trialling libraries usually means just creating a small program using the basic fea-
tures you’ll need. For example, you may need a high-quality random number generator
for your code, but you want it to be repeatable (give the same numbers every time).
You may try creating a small program to seed the generator and then print a few ran-
dom numbers. You run this a few times and make sure that you get the same numbers,
and if you have any problems, you can diagnose and fix them in a small piece of code
rather than a larger one. This is sometimes called a minimum working example.
Trialling an algorithm is more work, but if you think ahead you can simply insert
the prototype code into your main code.8 You write up your implementation, and test
it on some simple data, and check the result. This is also a good time to check the
performance of both the algorithm and your version of it, if that is going to be critical.
Do not optimise the algorithm at this early stage, but do not be afraid to
throw it away if it will clearly not serve your purpose.
1. Comment first - start writing, in plain language comments, what each piece of
code should do. Go around a few times, adding more detail, until you know
“exactly” what to code. “Exactly” is in quotes because how exact you need to
be depends on what you’re writing and your personal preference. You may want
each line fully described, or you may be happy with comments such as “Write the
data to the file”.
2. Files and functions - start by creating the files you’ll need, breaking your code into
chunks with a shared purpose. For example, you may want a “user interaction”
module, or an object representing a physical object. Then fill in the functions
signatures you know you need. Then gradually code your “main” routine, creating
functions as you need them, but not filling them in. Then start filling in layers of
functions.
3. Just do it - for simple scripts, problems you already know how to solve, or perhaps
counter intuitively, problems you have no idea how to solve, you can just sit down
and start writing, looking up how to do things as required. This has obvious
advantages for the first two sorts of problem: for the third attempting to plan in
detail is impossible without a lot of reading, and you may feel it is more productive
to learn as you go. Remember that you may eventually need to refactor, or even
rewrite your code in this approach, so allow time for this.
Even 1 can be magic, if it occurs because you have 0 and 1 based arrays
• The golden hammer - when all you have is a hammer, everything looks like a nail
Don’t force your favourite language/tool/technique to do everything
But don’t spend time learning a new tool if yours is adequate
• Multiple return types - in interpreted languages like Python, functions can return
any type of value, different types in different circumstances
Returning a dictionary with varying keys is OK unless you need particular
keys
Returning an array (if many results) or a single value (if one result) is OK
with caution but it may be more trouble than it’s worth
In general don’t return strings in some cases and numbers in others, such as
a sqrt function which gives either a number or the string “Bad input”. Instead
consider returning a struct or class containing both items. You may still make
mistakes, but they’ll be less weird.
This chapter so far has given a rapid summary of some rules and guidelines and general
ideas, and also some of the red flags to beware of when designing and creating code.
Most of this is based on our experiences with scientific software. As the section quote
notes, no detailed plan survives sitting down and actually writing, which is a major
motivation for agile development and its brothers. Even worse, in real situations you
often don’t have the luxury of using all of the best practices. This section gives a few
examples of practical scenarios where some part of our ideals has to be relaxed, and
one has to choose which parts to retain.
One useful model for doing this is to label everything with its MoSCoW method
specification. This means deciding for each item whether it must be included, i.e.
without it the project is deemed a failure; should be included, i.e. it adds real benefit
and is strongly desired; could be included, i.e. it would be nice to have; and wont be
included, i.e. it is not required at this time.
functions call which others). Many can also generate todo lists, like the Extensions list
in the image.
Some common tools are listed in A.0.5 for various languages.
1 MODULE n u m e r i c a l f u n c t i o n s
2 FUNCTION max( INTEGER n v a l u e s , ARRAY v a l u e s )
3 FUNCTION a v e r a g e
4 FUNCTION g e o m e t r i c a v e r a g e
5 // These names g i v e some b a s i c i n f o r m a t i o n about t h e i r f u n c t i o n s
6 MODULE i o
7 FUNCTION p r i n t (STRING t e x t )
16 CHAPTER 1. INTRODUCTION TO SOFTWARE DEVELOPMENT
1 MODULE f u n c t i o n s f o r f i n d i n g b a s i c n u m e r i c a l r e s u l t s b y j i m
2 FUNCTION m a x i m u m v a l u e o f a n a r r a y (INTEGER
i n t n u m b e r o f v a l u e s i n t h e a r r a y , ARRAY a r r v a l u e s )
3 FUNCTION a v e r a g e o f t w o n u m b e r s
4 FUNCTION g e o m e t r i c a v e r a g e o f t w o n u m b e r s
5 // These names a r e l o n g and i r r i t a t i n g t o type . The v a r i a b l e names r e p e a t
t h e type i n f o r m a t i o n u n n e c e s s a r i l y
6 MODULE i n p u t a n d o u t p u t
7 FUNCTION p r i n t t e x t (STRING t e x t t o p r i n t )
8 FUNCTION p r i n t t e x t t o n a m e d f i l e (STRING t e x t t o p r i n t , STRING
f i l e n a m e t o p r i n t t o , FLAG p r i n t n e w l i n e a t e n d o f t e x t o r n o t )
9 //The names a r e v e r y l o n g a g a i n and much i n f o r m a t i o n i s redundant ( both
p r i n t t e x t t o n a m e d f i l e and f i l e n a m e t o p r i n t t o t e l l us t h e same
thing )
10 //The f l a g name has become v e r y long , and t e l l s us what we ( s h o u l d ) know
from t h e type , t h a t t h i s i s a t r u e −or−f a l s e f l a g whether t o do X o r
not
Self-documenting style does not mean you do not have to document your
code. The examples I show above as “good” do not completely describe the functions,
and as the last snippet shows, if you try to do this you quickly get too verbose and
complicated.
• Think Principle of Least Surprise (PLS) and try to name things to save everybody
time and effort, especially youself
• Don’t repeat information already given by variable types, such as the logical flag,
or the fact that a parameter is passed by reference
4 Parameters
5 o v e r r u n The nurney t o p r o c e s s
6 d e c i m a t e Reduce t h e nurney a m p l i t u d e by a f a c t o r t e n b e f o r e p r o c e s s i n g
7 Returns
8 C a l c u l a t e d nurney a m p l i t u d e
9 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
10 const double minimum entropy = 0 . 7 0 7
11 C r i t i c a l e n t r o p y below which r e s u l t s become u n r e l i a b l e and f i m b r i a t i o n
i s i n d i c a t e d . This parameter can be tuned t o r e d u c e t h e
c a l c u l a t i o n overhead . For b e s t r e s u l t s u s e a v a l u e between −1.2 and
23
4 #hex c a l c u l a t e
5
6 ’ Out o f c h e e s e ’ e r r o r s imply you have i n s u f f i c i e n t a n t s t o run a t t h i s
time . Try
7 #hex i n i t i t a l i s e BRL
8 I f problems p e r s i s t , p l e a s e r e i n s t a l l U n i v e r s e .
6 Parameters
7 o v e r r u n The nurney t o p r o c e s s
8 d e c i m a t e Reduce t h e nurney a m p l i t u d e by a f a c t o r t e n b e f o r e p r o c e s s i n g
9 Returns
10 C a l c u l a t e d nurney a m p l i t u d e
11 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
12 const double minimum entropy = 0 . 7 0 7
13 C r i t i c a l e n t r o p y below which r e s u l t s become u n r e l i a b l e and f i m b r i a t i o n
is indicated .
6. Simple config files - e.g. ini files, json files, Fortran name lists or files of key-
value pairs
One code we work on uses 5 different of these for various purposes, and 2 or 3 is not at
all uncommon.
Hard coded values have plenty of use for parameters you change only rarely. For
example you may write log information into a file called run.log in the specified work-
ing directory. Prompting the user should be used sparingly and consider offering an
alternative for somebody running your code via a script.15 Control by file existence is
especially useful for causing a long-running code to abort. You simply check every-so-
often for the presence of a file, e.g. STOP.
Environment variables share some perils with global variables and should be changed
only with caution, but can be very useful for simple global config information. Compiler
flags are invaluable for including or excluding debugging code, or selecting a code-path
at compile time for performance. They can also be used to select a variable type at
compilation (such as float versus double). They do introduce more code paths to test
though so should not be overused.
Command line options allow a lot of flexibility and are perfect for programs invoked
using a script. For example, when you compile your code, you probably supply the -o
argument to specify the output file name. Problem sizes, input-file names and behaviour
can all be controlled this way.
15
They can pipe input to your code, but it is not always easy, especially in Python
20 CHAPTER 1. INTRODUCTION TO SOFTWARE DEVELOPMENT
ini example
1 [ control ]
2 u s e f l o a t =1
3
4 [ output ]
5 rolling output = true
to complex nested structures such as XML or JSON files. JSON libraries exist in many
languages and are a very good option in general. If you want to, you can embed either
the entire config file, or some signature16 of the file into the output.
Finally, writing good, robust GUIs is hard and relatively uncommon for scientific
codes. In particular, direct control through a GUI is usually a poor choice for a code
running on a cluster, as you may have to wait for your job to schedule. A GUI to create
an input file can be useful, but is usually a late addition. For very complicated codes
you may even consider a scripting interface, for example using Python, Ruby or Lua
to setup and run your code. However, these options are out of scope for these notes.
Note also that a live interface brings back the issue of parameter preservation unless
you carefully output the configuration before running.
Always validate your inputs. What this means in practice can vary, but in general,
do not trust anything supplied by a user, even if that is you. This is not be-
cause users cannot be trusted, but mistakes and ambiguities can happen. For
example, imagine your code starts by deleting any output files in its working directory.
You may write something like
1 FUNCTION c l e a n u p d a t a (STRING r e l a t i v e w k d i r )
2 STRING f u l l p a t h = ” . / ” + r e l a t i v e w k d i r //Assume working d i r e c t o r y i s
subdirectory of current directory
3
4 system . e x e c u t e ( ’ rm −r f ’ + f u l l p a t h ) // Use system d e l e t e t o remove a l l
f i l e s i n working d i r e c t o r y
5 END FUNCTION
16
e.g. an MD5 hash of the config file, but note that insignificant changes, such as whitespace, will
change this
1.7. GETTING DATA IN AND OUT OF YOUR CODE 21
Now imagine your user forgets or fails to supply a directory name to your code. This
function will happily delete any and all .dat files in their current directory. Worse,
imagine what happens if they give you a directory with a space at the start! If you
forget to trim this out, this code will endeavour to delete everything in their current
directory and then everything in the working directory! This may sound silly and
contrived, but worse things have happened.17
As another example, suppose your code is intended to convert from one file format
to another, and you ask for two file names from the user, in and out. What happens
if they give the same name for both? Will your code cope? What about if you ask for
a number dictating an iteration count and the user gives you a negative value? Will
you get an infinite loop? What if you ask for a size and the user specifies a very large
number? Will your code eat all their memory and crash? Finally what about the classic
“SQL injection”, as in Fig 1.2 where names have been input and then inserted into a
database without input sanitation. A crafty user can instruct your database to delete
all its data. The last example is an entire course by itself, but in general: be nice to
your users and yourself and check inputs for sanity before proceeding, and always be
extra cautious when deleting or overwriting files and data.
Figure 1.2: SQL injection in practice. Always check your inputs!! Perma-
link: https://imgs.xkcd.com/comics/exploits_of_a_mom.png Creative Commons
Attribution-NonCommercial 2.5 License.
If you are reading large chunks of data, for example reading in a restart file (1.7.6)
you will want to consider using MPI IO to allow collective reads. Alternatives are
reading the entire data set in on one processor and broadcasting it (needs enough
memory for the entire set on every processor), or reading in chunks on one processor and
sending each chunk in turn to the relevant process (inefficient, one send per processor).
MPI-IO allows multiple processes to read files at once and ensures they do not interfere
with each other.
5. Full data file formats - for example fits files (in Astronomy), HDF5
1. Do files need to be human readable? Binary files can be half the size of the
equivalent text file, but you need to either know their content or use a specific
binary format
2. Will the files be kept for long periods? Beware of choosing custom formats that
may change or be deprecated
• Does my software use other software? What are its terms? Do I include their
code in mine?
• Do I want attribution when others use my code? What about if they share it?
• What about if they take it and edit it, perhaps producing their own programs
they also share?
anti-pattern Patterns for the dark-side: models of code which are inflexible, restric-
tive, or unreliable common approaches to a problem. C.f. pattern, 11
coding standard A set of rules dictating anything from techniques (some companies
forbid pointers), naming conventions, source code layout (2 spaces or 4? Do
braces go on a new line?) and every other element of code style. The coding
standard may select a language standard but shouldn’t cover matters of source
code validity. 16
DRY Don’t Repeat Yourself, the idea that you should try and reuse code as functions,
modules, libraries etc rather than copy paste and edit. 3
input sanitation Removing active code or invalid characters from input. For ex-
ample, suppose you were to (please don’t ever do this without extreme cau-
tion!!) take some user input and execute it, as all sorts of online bots do.
Now suppose you feed this directly to the system and your user enters “firefox
http://my super virus downloader.co.uk”. 21
MUU Minimum Useful Unit; the smallest functioning, useful, releasable version of a
program or feature. 9, 10
26 Glossary - Software Development
pattern Models of code which are flexible, general purpose, reliable common ap-
proaches to a problem. C.f. anti-pattern, 11
PLS Principle of Least Surprise; do the least surprising thing in case of ambiguity. 16,
36
refactoring Changing the content of code without changing its results, c.f. rewriting.
For example moving some chunk of code into a function; placing several named
variables into a single structure; renaming things for better consistency etc. 13,
26
rewriting Rewriting differs from refactoring as you may actually change results. Chang-
ing to a different inexact algorithm would generally be a rewrite, as would chang-
ing from plain-text output files to a special file format. 13, 26
WSIWYG What You See Is What You Get, editors like Word where you see format-
ting and arrangement as you go, as opposed to e.g. Latex where you must compile
your document to see it laid out. 7
YAGNI You Aren’t Going to Need It, a feature which may seem cool but isn’t actually
required right now, and probably never will be. If it ever is, your current version
of it probably wont work anyway. 3
Chapter 2
Debugging is twice as hard as writing the code in the first place. There-
fore, if you write the code as cleverly as possible, you are, by definition, not
smart enough to debug it. - Brian Kernigham
The definition of “bug” is “an error, flaw, failure or fault in a computer program
or system that causes it to produce an incorrect or unexpected result, or to behave
in unintended ways.” [Wikipedia (Software bug)] Note that not every example of a
program “misbehaving” is a bug, depending on how one defines “unexpected”. Some-
times there may be several valid ways of doing something, or the programmer may have
chosen a compromise. Compromises should be covered in code documentation.
Make sure to consider this in your own code, as you may not remember
your reasons in a few months time.
As we mentioned earlier, undefined behaviour in C, “system dependent” behaviour
in Fortran and in Python the absence of a standard and subsequent platform-dependence,
are very important to watch for. What they mean is that your program may work cor-
rectly on your machine, but on another system may fail, and not necessarily in any way
you might notice. Bugs which you can reproduce are troublesome - bugs which you
can’t are awful. Not only are you working in the dark to diagnose it, you can never be
sure it has gone!
27
28 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
• poor performance
Examples:
• Finding the minimum of a list the wrong way: if your data is sorted, the minimum
value must be the first in the list but sorting the data to just find the minimum
item is silly.1
Notice that the case of a == 0 has been missed, and in this case c is undefined.
• Most typos: mistyped names, function assignment rather than call (Python),
missing semicolon (C). Mis-use of operators, for example using & and && (C).
32 bit and 64 bit being common. For 32-bit floats, 8 bits are used for the exponent, 23
for the significand3 and one for the sign. This introduces several limitations. Firstly,
for large numbers it can be the case that X + 1 == X to the computer4 , because with
only 23 bits, some of the number has been truncated. Secondly, they can’t exactly
represent all decimals, even those which terminate in normal, base-10 representation.5
And finally, sufficiently large numbers cannot be stored at all, and will overflow.
the value 3.0/2.0 is calculated 1.5 and then stored into the integer a by truncating the
non-integer part. Most programming languages truncate rather than round, i.e. they
throw away the decimal part. This can lead to odd results in combination with floating
point errors, for example the example below where 0.5/0.01 can be slightly less than
50, and so when truncated becomes 49.
Rounding errors are one of the reasons it is a bad idea to use floating point numbers
as loop indexes, even if your language allows it. This snippet
1 REAL i
2 FOR i = 0 . 0 , 1 . 0 , 0 . 0 1 DO // Loop s t r i d e i s 0 . 0 1
3 PRINT i
4 END
can end up with i slightly less than 1 at the expected end point, and you may then
sometimes get an entire extra iteration.
reduce these rounding and truncation errors. Secondly, if you take two such sums and
subtract them, you can get strange cancellation results and unexpected zero or non-zero
answers, see the example below.
a/b is 0.5 exactly, as is d/e. However, in the first case, a and b are integers so the
result of their division is converted to an integer, here by truncating (throwing away
the fractional part) to give 0. This is the case even when we ask it be stored into a
decimal, c. d and e are floating point numbers, so behave as we expect. In complicated
7
In normalised scientific notation 1.00 × 10x and 9.99 × 10x are both valid but e.g. 0.10 × 10x is
not.
8
Suppose 5 ”slots” are available: 1.2345 × 10x has more sig.fig than 0.0123 × 10x+3
9
Python 3 promotes all divisions to be float, and uses the special operator // for integer division.
2.2. BUG CATALOGUE 31
expressions it can be difficult to work out which parts might be integer divisions, so
it is a good idea to explicitly convert to floating point to be sure. Note that similar
conversions occur if you mix 32 and 64 bit (or any other sizes) of number. The rules
can be complicated, so again it is a good idea to keep everything the same or you may
get unexpected loss of precision.
What is NaN?
NaN(nan in Python) is a special value indicating that something is Not a Number.
NaNcan result from e.g taking square-root of a negative number, as there is no real val-
ued solution. NaNis contagious - any calculation involving a NaNwill have NaNresult.
However NaNhas one special property - it is not equal to any other number, including
itself, and does not compare to any of them either. Beware though: this is true for
any other comparison too, so NaNis not less than NaNnor greater than it. Figure 2.1
shows which arithmetic operations can result in NaN.
Symptoms:
Examples:
• In really bad cases this even breaks associativity: 1.0 - (1/33.0 + 2/33.0 + 29/33.0)
- 1/33.0 may not give the same answer as 1.0 - (1/33.0 + 2/33.0 + 30/33.0)
• Divide by zero: in Python this is an exception. In gfortran you can set floating-
point exception traps using -ffpe-trap etc
32
Left
operand
is
in
the
column,
right
along
the
top
+ -‐inf
-‐1.0 -‐0.0 0.0 1.0 inf nan * -‐inf -‐1.0 -‐0.0 0.0 1.0 inf nan
-‐inf -‐inf -‐inf -‐inf -‐inf -‐inf nan nan -‐inf inf inf nan nan -‐inf -‐inf nan
-‐1.0 -‐inf -‐2.0 -‐1.0 -‐1.0 0.0 inf nan -‐1.0 inf 1.0 0.0 -‐0.0 -‐1.0 -‐inf nan
-‐ -‐inf -‐1.0 -‐0.0 0.0 1.0 inf nan / -‐inf -‐1.0 -‐0.0 0.0 1.0 inf nan
-‐inf nan -‐inf -‐inf -‐inf -‐inf -‐inf nan -‐inf nan inf inf -‐inf -‐inf nan nan
-‐1.0 inf 0.0 -‐1.0 -‐1.0 -‐2.0 -‐inf nan -‐1.0 0.0 1.0 inf -‐inf -‐1.0 -‐0.0 nan
-‐0.0 inf 1.0 0.0 -‐0.0 -‐1.0 -‐inf nan -‐0.0 0.0 0.0 nan nan -‐0.0 -‐0.0 nan
0.0 inf 1.0 0.0 0.0 -‐1.0 -‐inf nan 0.0 -‐0.0 -‐0.0 nan nan 0.0 0.0 nan
1.0 inf 2.0 1.0 1.0 0.0 -‐inf nan 1.0 -‐0.0 -‐1.0 -‐inf inf 1.0 0.0 nan
inf inf inf inf inf inf nan nan inf nan -‐inf -‐inf inf inf nan nan
nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan nan
• Small differences of large numbers: (1.0 + 1e-15 + 3e-15) - (1.0 + 4e-15) is not
zero in e.g. Python2, but (1.0 + 1e-15 + 3e-15) - (1.0 + 4e-15 + 2e-16) is.
• In Python all real numbers are “double precision” so run up to about 1.8e308.
Integers will automatically grow to hold their content BUT they will get poten-
tially a lot slower when they do. In C or Fortran you specify type, float/double
and int/long. Try this:
1 int j =2147483647;
2 p r i n t f ( ”%d\n” , j ) ;
3 p r i n t f ( ”%d\n” , j +1) ;
The value j is set to is the maximum possible 64 bit integer, so the +1 “overflows”.
The reason it becomes negative is because of how integers are represented. Note
there are also “unsigned” integers, which overflow to 0.
• Instability: some algorithms work analytically but when numerical effects like
rounding are included can become unstable and give a wrong or no answer
Examples:
• Undefined variable: variable gets whatever value that bit of memory happened to
have. Code may give the wrong answer, may vary run to run, may be changed if
unrelated code is edited
34 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
• Undefined pointer: a pointer gets whatever value the memory had. Usually leads
to a segmentation (seg) fault if you try to access it. Always nullify your pointers.
• crashes (usually only if you write to completely invalid memory, or exceed available
memory)
Examples:
• Off-by-1 error in array access:
1 ARRAY, REAL( 1 0 0 ) a r r
2 arr [ 0 : 1 0 0 ] = 1
Remember that different languages use 0 or 1 for the first element of an array.
• Buffer overrun errors: (C strings, C++ if you use C-style strings etc)
1 char name [ 1 0 ] = ”Abcdefghij” ; // No s p a c e f o r n u l l t e r m i n a t o r ,
s t r i n g i s now i n v a l i d
2 p r i n t f ( ”%s” , name ) ; // See s t r i n g c o n t e n t and then some junk
• Using a freed pointer: When objects are freed, their memory is marked free, but
is not changed until it is allocated to something else.10 If you are lucky, use of
a freed pointer will segfault or show up fast. If you are unlucky, it will work
perfectly as the “ghost” of the data is still there, and show up only as subtle
bugs. Always nullify pointers after freeing the memory they point to
10
This may be familiar if you have ever accidentally deleted a file - often the file is still there on the
disk and can be restored, but any new files written may overwrite it.
2.2. BUG CATALOGUE 35
• Mis-allocation of memory:
1 int ∗ s t a r t = m a l l o c ( 1 0 2 4 ∗ 1 0 2 4 ∗ 1 0 2 4 ∗ 3 ) ; // Exceeds MAX INT, a c t u a l l y
a t t e m p t s t o a l l o c a t e a n e g a t i v e number
Note that your compiler is unlikely to warn you about this, even if it detects the
overflow
• File IO
1 INTEGER i
2 ARRAY data [ 1 0 0 ] = GET NAMES( )
3 FILE o u t p u t f i l e
4 FOR i = 0 , 99 DO
5 OPENW o u t p u t f i l e //Open f i l e f o r w r i t i n g
6 data [ i ] = TO UPPER CASE( data [ i ] )
7 WRITE o u t p u t f i l e , data [ i ]
8 CLOSE o u t p u t f i l e
9 END
• Multiple loops where one would do, and/or loops where array ops would do
1 INTEGER i
2 ARRAY data [ 1 0 0 ] = GET INTEGERS( )
3 FOR i = 0 , 99 DO
4 data [ i ] = data [ i ] + 1
5 END
6 FOR i = 0 , 99 DO
7 data [ i ] = data [ i ] ∗2
8 END
Usually the overhead of the loop is not much, but in these cases with very little
work to do, it can be significant. These examples could also be replaced with
a single array operation (in Fortran, Python, C++) which can be much more
efficient.
Since the only purpose of this loop is to identify whether the given value exists
in the data, we may as well BREAK on line 7, as continuing the loop is needless
work.11
operator” in effect returns its second argument, so you get b=10 and a is untouched.
In Python, a,b is interpreted as a tuple and the interpreter looks for two values on the
right and doesn’t find them, giving an error. In Fortran, a,b is simply invalid.
Ultimately, most languages are trying to be consistent, to avoid context where it is
not needed13 , and to be able to do very complicated things. Sometimes this does make
simple things hard. But if you find yourself wondering “but why” very often,
consider whether you have got the right tool for the job.
On two occasions I have been asked, “Pray, Mr. Babbage, if you put into
the machine wrong figures, will the right answers come out?” ... I am not
able rightly to apprehend the kind of confusion of ideas that could provoke
such a question. - Charles Babbage
This sounds silly, but you may be surprised how often a “bug” turns out to be almost
exactly this but in reverse. You are certain that the code is correct, and the input
data is correct, and yet the wrong answer comes out, again and again. To paraphrase
13
I.e. wherever possible a chunk of code has the same meaning regardless of the surrounding code.
14
Quoted from http://americanhistory.si.edu/collections/search/object/nmah_334663
38 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
Sherlock Holmes when you have eliminated the impossible and still not found
the answer, perhaps it wasn’t as impossible as all that. Keep this in mind when
debugging. Computers are very complex, but they are also stupid. They do as they are
instructed. Sometimes this is not what you thought you had instructed them to do, and
very, very occasionally it is truly incorrect, but your aim is usually to find where
what you thought would happen diverges from what actually happens.
Remember, when debugging, you are in good company. The founders of computing
wrote buggy code, and some of the key developments in early days was learning how
to test, validate and fix it, as for example
There are two ways to write error-free programs; only the third one
works. – Alan Perlis, ”Epigrams on Programming”
Can you guess why? It is not uncommon to mistakenly type only one “=” sign inside
the if. If this happens, then x = 10 is perfectly valid and will compile and surprise you
by always executing the body of the if. Swapping the order makes this invalid. Note
that this trick is no longer needed, as compilers will warn you about bugs like
accidental assignment instead of comparison if you let them.
2.6. BASIC DEBUGGING 39
1. Turn on all compiler errors and warnings, and recompile/run your code. If this
stops the error occurring, see Sec 2.6.3. Otherwise continue these steps.
3. Place some output statements roughly throughout the troublesome code, or your
main code if you have no idea where to start
Simple prints allow you to check whether code is reached or executed
Printing values of variables helps you check for undefined or wrong data
If your code exists with an error you may get a backtrace. This usually
tells you the line where the failure occurs and gives you a starting point for the
problem.
4. Begin to drill down to the source of the problem, placing output statements more
tightly around the problem and running again
The fastest way to find the problem is probably bisection again.15 There’s no
rule for deciding what “half” means though: sometimes it is a number of lines of
code, sometimes it is some kind of “functional unit”
Everybody has, at some point, struggled with a problem for hours getting nowhere;
giving up and asking for help, you find that as soon as you explain the problem the
solution becomes obvious. The importance of the act of explaining is well know, and for
tricky programming problems, there is no substitute for talking the thing through with
another coder. Often, especially as researchers, there may not be anybody you can do
this with. The commonest suggestion is to use a rubber duck.16 Anything will do, but
the key is to put the problem into words, preferably aloud. Explain what is happening
and what should be happening. Often you will find that your have the answer before
you even finish the question.
another reason why print-debugging should be in your repertoire, as some sorts of bug
just do not occur inside the debugger.
Then to enable debugging you set the global to be True and rerun your code. This can
also be done in compiled code, but remember that it does have some cost.
A more flexible option is to use a library designed for logging purposes. The Python
logging module is good for this. You designate statements as Errors, Warnings, Debug
etc, and then set the logging level. These modules benefit from letting you print to
screen or to a file or not at all, from adding dates and source code line info, and from
keeping log info separate to information you want to display to a user.
While the quote above is a joke, there is some truth in it: once a shortcoming of
code is documented, it is no longer strictly a bug. You may encounter bugs in bug
trackers that are labelled “wont fix” or similar. This usually means there is a known
bug, but that it will not be fixed. Often these were the result of a design decision which
could have been made differently, but cannot now be amended, either at all or for a
reasonable time, effort or complexity cost. On the other hand it is very annoying as a
user to find that you have wasted time on something that will not work.
In your code, you are likely to make assumptions and approximations, and to have
to choose between simpler, easier to implement algorithms and more complex ones. For
example, suppose you had to implement code to calculate xy . The simplest way to
do this is by repeated multiplication: xn = x ∗ x ∗ ..x where n is an integer. If your
17
Name your file .F90 (capital F) to indicate the preprocessor should run
42 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
code only ever needs to calculate positive integer powers, it is quite reasonable, and in
fact recommended, to use this simple version. It is faster, simpler, and fit for purpose.
However, you must document that a limitation exists and you should consider
adding code to check for violation of your limitations.
Asserts are one way to ensure that you are dealing with types and values that you
are able to. This is especially useful in languages like Python where you do not specify
types directly. For example:
1 FUNCTION power (REAL x , INTEGER n )
2 ASSERT( n > 0 ) //n must be p o s i t i v e
3 power = 1
4 FOR i = 1 , n DO power = power ∗x
5 RETURN power
6 END
7
8 INTEGER n = 3
9 REAL x = 2 . 0
10
11 PRINT power ( x , n ) // P r i n t s 4 . 0
12 PRINT power ( x , −1) // A s s e r t i o n e r r o r
In languages that do not have an ASSERT, you can raise or throw an error if your
condition is not met, or return with an error value.
1 def power ( x , n ) :
2 a s s e r t ( type ( n ) i s int )
3 assert ( n > 0 )
4 power = 1
5 f o r i in range ( 1 , n ) : power = power ∗x
6 return power
7 print power ( 2 . 0 , 3 ) / / 4 . 0
8 print power ( 2 . 0 , 3 . 0 ) // A s s e r t i o n e r r o r l i n e 2 , n not an int
9 print power ( 2 . 0 , −1) // A s s e r t i o n e r r o r l i n e 3 , n not +ve
Formally, those things which must be true when a function is called are named
“preconditions”, and those things the function guarantees will be true when it completes
are called “postconditions”. Together these form the “contract” between the function
and the calling code.
As a second example, consider accessing an array. In C simple arrays do not know
their size; C++ provides the vector class, a 1-D array that can use checked or unchecked
access; In Fortran, array size can be checked, but is not by default; while in Python
out-of-bounds access is an error. The following snippet has several possible results,
depending on language and compiler options.
1 ARRAY, REAL( 1 0 0 ) data = GET DATA( )
2 PRINT data [ 1 0 1 ] //May p r i n t junk , throw an e r r o r , c a u s e seg−f a u l t
Remember that each check adds overhead, so performance focused languages tend to
have them only as an option, while more general purpose languages make them always
apply. Checks like these are the foundations of code testing. You want to know how
things behave when given good and bad inputs, and be sure about what is going on.
2.8. TESTING PRINCIPLES 43
We want to pick some input values that will let us be sure the function works. Clearly
we thought about the negative n case, so we should test that. 0 is often special in
maths, so we probably want to check x of 0, and check that n = 0 gives the expected
assertion error. If we look at line 4 we notice that our loop is from 1 to n, which suggests
we should also check n = 1 (to make sure the loop runs once, not 0 times) and n = 2.
This is the principle of checking the boundaries. This seems to be a comprehensive set
of values, but we do need to check them all.
There is something crucial about what we have done here. We have carefully crafted
our test values based on how we know the function works. This is sometimes called
“white box” testing (in contrast to a “black box” where the code internals are unseen).
“Aha” we now think. We checked everything that can go wrong! We know this function
works all the time. In this simple example, we were able to do this. In more complex
examples it is not so easy to spot all the possible things that can go wrong.
A classic example of fatal software bugs is the Therac-2518 Therac was a radiation
therapy machine, delivering electron-beam and X-ray treatments in the 80s. Previous
versions had complex systems of hardware interlocks to ensure the proper shielding
plates were in place, and that no unsafe doses could be delivered. Therac-25 removed
these in favour of a computer system. Between 1985 and 1987 7 people were exposed
to massive radiation overdoses. 2 or 3 died19 and the others suffered serious and life-
changing injuries.
Primarily, the Therac-25 disaster is an example of a terribly wrong solution. The
removal of hardware interlocks on such a critical system was risky. Even your kitchen
microwave has a hardware switch to turn it off when the door is open. In some senses
it is an example of a lack of proper testing, for example, requesting one mode, and
then requesting another within 8 seconds, while the system was still moving pieces into
18
(e.g. (Contain some medical details) https://hackaday.com/2015/10/26/killed-by-a-
machine-the-therac-25/, http://courses.cs.vt.edu/professionalism/Therac_25/Therac_1.
html)
19
One died of the cancer being treated, but would have at least needed major surgery otherwise
44 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
place, left the system in an indeterminate state. This could, and should, have been
caught. Another bug turned out to be a simple integer overflow.
Finally though, Therac-25 is an example of the real pitfall - only testing for the
errors that were thought of, not being able to catch all possible inputs, and assuming
that passing the tests meant correctness. To quote “In both of these situations, operator
action within very narrow time-frame windows was necessary for the accidents to occur.
It is unlikely that software testing will discover all possible errors that involve operator
intervention at precise time frames during software operation.”20 The machine was
tested, and was used for thousands of hours, and only a handful of incidents occurred.
For a not-completely-inaccurate version of the problem, imagine a simple system which
can swing a lead plate into position in front of the beam, and also activate the beam on
high or on low. The operator selects low mode and no shield plate and your software
begins to set up. The operator realises they selected the wrong mode, and changes
inputs. The software switches the beam mode, but fails to note that the plate position
was also edited. Nothing checks the settings are all consistent. The beam activates on
high, with no shield in place. Hardware interlocks activate after a small delay and the
system shuts off, but the machine readout calculates dose based on the settings it now
has stored and reports insufficient dosage, allowing the operator to press a single key
to fire the beam again...
Testing is incredibly powerful, and can catch all sorts of bugs. There are many
well developed strategies and theories of how best to do it. Some form of testing is
essential in your software. But there is also a golden rule you may not see written
down often: running tests tells you that the tests pass, not that the code is
correct. You must work hard to make this the case, not suppose it. with
immediate corollary The tests you really need to run are probably the ones
you haven’t thought of, because you never realised that could be a problem!
Unit Testing
Unit testing is testing each building block of your code in isolation, for example each
function. You provide inputs and check the outputs. You may focus on testing the
domain of functions, i.e. that they behave properly for all parameters, or you may
focus on testing their correctness for common parameters, or accuracy across a range.
Formally you should provide “mock” versions of all inputs, but in practice there are
likely to be certain inputs you want to focus on.
20
http://courses.cs.vt.edu/professionalism/Therac 25/Therac 4.html quoting Miller (1987)
2.8. TESTING PRINCIPLES 45
Figure 2.3: A simple integral using the trapezium rule, and the rough scaling of the
error with the step size for x1 = 0.5, x2 = 1.0
Continuous Integration
Continuous Integration in its pure form means merging work from multiple developers
regularly, running automatic code building and testing steps, and rejecting code that
fails. For large projects with many developers, this ensures that people do not make
incompatible changes, and that there is always a working latest version of the code
ready to go. For our purposes we’re interested only in the automatic-build-and-test
element.
Regression Testing
Regression testing is perhaps the easiest sort of test to implement in scientific code. At
the basic level you create a few test cases with known answers, either analytically or
based on a first working version of your code. Each time you make significant changes,
you run the code on these problems and compare the final answers. If the answers differ
by too much then the test fails. Note that too much depends on circumstances. For
example, consider a simple integral like this:
Z x2 X
x2 dx ' 0.5(x2n + x2n+1 )∆x
x1 n
as illustrated in Fig 2.3. We know the analytic solution, so we can work out the
percentage error for different step sizes on the interval [0.5, 1.0]. You may recall from
numerical analysis courses that the error scales as (∆x)2 . Now suppose your code per-
forms an integral like this. Changes to ∆x will change the exact solution you find, but
there will be some range of acceptable answers. Be careful when setting error in-
tervals: absolute values are useful for things like floating point errors, but in
most we probably want to use an error percentage otherwise we have much more
stringent constraints on the function 10x2 than we do 0.1x2 Unless your requirement
46 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
Purpose Tolerance22
64 bit machine precision23 2−52 ' 2.22 × 10−16
32 bit machine precision 2−23 ' 1.19 × 10−07
Exact calculation equality (64 bit)24 10−12
Variant calculation equality (64 bit)25 10−10
Numerical calculation equality (64 bit)26 10−6 to 10−8
Approximation equality 27 10−3 to 10−5
Table 2.1: Typical tolerances for numeric results. The most important rule is to allow
tolerances at least 10x smaller than the effect size you are seeking.
is bit-wise exact reproducibility, (page 46) do not compare floats for exact
equality in your tests. There are circumstances where code like the following can
print FALSE even though all the numbers are exact21
1 REAL a = 1 0 . 0 , b = 5 . 0 , c = 2 . 0
2 PRINT b∗ c == a
This can cause trouble if you change to another compiler or machine which behaves
subtly differently.
Test Coverage
Test coverage, or code coverage, is the fraction of code which is included in tests. Ideally
all of your code would be tested, but in practise there are often rare combinations
of errors that you never expect will occur, and some functions which are difficult or
unproductive to test. Note that coverage refers to code, not inputs. 100% coverage
21
This is quite rare, but can happen when one side of the expression is stored in “extended precision”,
currently usually 80 bits. The “extra” bits are irrelevant and will be truncated when the result is put
back into a normal 64 or 32 bit variable, but at the point of comparison they are not the same.
22
These rules of thumb are the ones HR uses in general code.
23
For numbers approximately 1, see machine epsilon
24
I.e. the same calculation performed with different code
25
I.e. slightly different analytical calculations
26
I.e. a good to excellent numerical result
27
I.e. two different approximate answers
2.9. TESTING FOR RESEARCH 47
doesn’t mean all possible inputs are considered: that might not even be possible. 100%
coverage still doesn’t guarantee correctness!
Performance Testing
Once your code works, and not before, you may want to consider profiling.
Many tools are available to help you find which parts of your code are dominating
its run time so you can improve performance. Usually they give you a graph of the
percentage of time spent in each function, often in the form of a tree.
1. Unit test the basic functions of your code where practical. For example, test your
numerical solvers in isolation. Test your data structures.
Errors deep in your code are the hardest to find, so aim to be confident in the
building-blocks before testing further up.
2. Test the domains of all your basic function too. Make sure you get a reasonable
response if you provide bad input, noting that there may not be much you can
do except fail. At least try to provide an error a user can understand.
3. Test larger chunks of code in the same way.
4. Run any available analytical test problems and check for a reasonable answer.
Try to cover all of the basic functionality for a few likely parameters.
5. Test for regressions as you modify your code. Make sure your answers are not
changing unduly.
In particular it is a good idea to keep test problems around that previously
broke the code or gave wrong answers.
6. Test on each new target problem against previous published results.
For real questions there is probably no test problem of use, so you’ll have
to check against other people’s work. Look for general agreement and remember
that you are unlikely to reproduce their answers exactly unless you are using the
same code and inputs.
• Unit tests, analytical results. We test our maths functions in isolation, making
sure we get the right answers for classic known values, such as sin π or exp 1.
• Domain testing. At the same time we check that invalid inputs gives us back a
sensible error rather than crashing the program.
• Unit tests. We want to be sure we’re taking user input correctly and printing
it out correctly too. If we misread input expressions we’ll never give the right
answers.
2.9. TESTING FOR RESEARCH 49
• Chunk testing, analytical results. While we could write unit tests for all the bits
in our expression parsing code, we might decide to simply test the final result
against known correct results.
• Analytical results. Once we’ve got mostly working code, we try using the program
on simple test problems, like the sin π example, but now using the user-interface
and parsing code.
• Regression testing. We keep some example inputs around, along with their out-
puts. We also keep some inputs that caused errors previously to check we don’t
reintroduce those bugs.
Each term is smaller than the last, so the sum looks like it is converging, but it actually
grows without limit as n gets large. Thankfully this situation rarely arises in practice,
but you must be cautious with convergence if you do not know your algorithms are
stable and convergent.28
Exactly the same process can be carried out with global parameters of an entire
code, such as a grid spacing. Remember that any two points can be joined by
a straight line so you need at least 3 points for convergence, preferably more.
If your quantity of interest seems to be converging, as in Fig 2.4, you can estimate the
“true” value and your error.
28
Further discussion is beyond the scope of these notes, but see any good text on numerical tech-
niques.
50 CHAPTER 2. PRINCIPLES OF TESTING AND DEBUGGING
2.10 Responsibilities
Extraordinary claims require extraordinary evidence – Carl Sagan, re-
stating many others
As researchers, publishing a paper is effectively saying “these results are true and correct
to the best of my knowledge”. So if you publish using a code that you wrote, you need to
be reasonably confident that it is “correct”. How much testing that requires varies. In
particular if you’re claiming some sort of wonderful new discovery, or presenting results
which might have real impact, you really must have a decent set of tests, and check
extensively against the current state-of-the-art results. For more ordinary results, you’d
test as thoroughly as you would some algebra or calculation, to avoid an embarrassing
correction or retraction.
If, or when, you start sharing your code with other researchers, the responsibilities
increase somewhat. You’ll need to be quite sure your code can’t cause chaos (like the
clause in nearly every EULA about damage to computer or data, or penalties resulting
from incorrect use of software). You also want to be as sure as feasible that the answers
are correct. And finally, you now need to consider the possibility of somebody using
your code “wrongly”. We’ve noted before that limitations and assumptions ought to
be clearly stated, and this is part of why. You’ll definitely need some sort of user
documentation at this point, covering all of this. You don’t want to stumble across a
paper and realise its wrong because your algorithm doesn’t work in that regime.
Since you have (right?) got a set of tests that validate your code, you can also
consider releasing those along with the code as a test-suite. Anybody who has your
29
See e.g. https://en.wikipedia.org/wiki/Uncertainty_quantification and links therein
Glossary - Testing and Debugging 51
code can then run these to confirm there’s nothing odd about their setup. Chapter 3
talks a little about testing frameworks which can help you here. The set you release
probably isn’t the entire set though, as you can set aside those which you only need
during development. End-to-end and regression tests are probably the most useful here.
Finally, any published result should be reproducible. This means capturing the
state of the code, it’s inputs, and any other essential information for every paper. For
the first item, version-control systems are ideal, and are discussed in Chapter 4. For
the others, see the section on Input and Output in Chapter 1.
To summarise:
correct A program that uses all language features that it uses correctly. It does not
necessarily give the answer that you expect. 27
floating point numbers Decimals, where the position of the decimal point is allowed
to vary. This gives them the same relative precision over a very large range of
values. Scientific notation is technically a floating point notation: although you
always write a fixed number of decimals, the scaling factor 10x means the absolute
accuracy changes. 28
garbage collector When variables go out of scope the memory they occupy should
be made available for reuse. In languages that allow references or pointers to
variables, there is a problem with this, as you only want this to happen after the
last reference to a given item is lost. This is done by the “garbage collector” in
languages which have one. Usually, this runs every so often, or when available
memory is getting tight, and cleans up all the now dead values. Note that lan-
guages like C don’t have this, you must free memory when the last pointer or
reference goes. In Fortran you must manually clean up Pointers, but Allocatables
are automatically deallocated (since F95) and cleaned up like regular variables.
34, 58
52 Glossary - Testing and Debugging
invariant A condition which is always fulfilled. For example, within any for loop (FOR
i =0, 10) you know that i is between 0 and 10. Often it is very useful to know
that a number is positive, non-zero etc as this allows you use code that relies
on this (e.g. if you’re passing the number to a sqrt function, or dividing by it
respectively).
machine epsilon Floating point numbers are stored in the exponential format a × 2b .
With a limited number of bits, there is a finite step from one number to the next
that can be represented. For example, in base-10 with a 5 digit significand (a) we
have 1.0000 × 10b and the next number we can show is 1.0001 × 10b . For b = 0 the
difference between these is 0.0001. For b = 4 the difference is 1. This step-size
is called the machine epsilon, and is usually quoted for numbers close to 1. For
very large numbers, the step size can be much greater than 1. This is one reason
why you should not using floating-point numbers for large integers. 28, 46
no-op Code that does nothing. Stands for no-operation, and can exist for many
reasons. Can include incomplete statements, like a; in C or conditionals like
if(false). 55, 104
overflow A numerical error caused by exceeding the largest number (positive or neg-
ative) a type can store. 30
regression Going backwards in code fitness, e.g. reintroducing a bug which was al-
ready fixed, making answer quality worse, breaking a working feature etc. 45
syntax error An error in the sequence of characters in a piece of code. For example,
a misspelled name, or a missing bracket, making the piece invalid and unreadable
to the computer.
underflow A numerical error caused by attempting to store a number that is too small,
often causing it to be rounded to 0. 30
Glossary - Testing and Debugging 53
unreachable code Code that never runs. This can be a function that is never called,
or a condition that is always true or false, so its body is unreachable. 55, 104
Chapter 3
Sec 2.2 went through the many types of bugs, with examples and their typical symp-
toms. Sec 2.6 discussed a basic strategy for debugging using print statements, while
Sec 2.8 discussed basic testing strategies. In this Chapter, we focus on some tools to
make debugging, profiling and testing quicker, easier, and more reliable.
3.1 ProtoTools
54
3.2. SYMBOLIC DEBUGGERS 55
Optimisation
Compilers have all sorts of competing demands to consider. Sometimes the main con-
sideration is the time taken for the compilation step. Sometimes it is the size of the
resulting program1 or its speed. Early compilers were comparatively simple, transform-
ing source into executable with some minor adjustments. Computer time was expensive
enough that complex compilation was not worth it.
Modern compilers are incredibly complex systems, because they strive to simplify
and optimise their output. Since this takes time, most give the option to set the level
of optimisation to use. For instance, the default is usually level 0, which makes only
minimal changes. Higher levels, commonly 1-3 can do things like inlining functions,
move parts of calculation out of a loop, or remove loops entirely, replacing them with
copies of the relevant code (loop unrolling).
There is one universal: the compiler will not reorder dependent pieces of
code; it can omit unreachable code and no-ops and can remove certain other
things.2 It is not absolutely the case that compiler optimisations can’t change the
behaviour of your code, but it is unusual. More commonly you have invoked undefined
behaviour and so there is no rule.
Usually you will want to debug at optimisation level 0, which does very little,
but sooner or later you will encounter a bug which disappears when you do. This
makes it particularly likely that you have forgotten to initialise something, or are doing
something ambiguous. This is another reason to avoid undefined behaviour rigorously,
because the bug often goes away when you run the debugger. Note that some compilers,
such as Cray, turn off all optimisation in debug mode, which makes finding this sort of
thing very tricky.
faults. In these cases, the debugger is an easy way to get a backtrace. The debugger
also doesn’t require you to recompile your code for each new “print”, which can be a
major advantage. Finally, in complicated cases the debugger lets you print details of
memory, and where variables are stored, which can be useful. You can also set variable
values to test bug fixes inside the debugger.
Print-like Debugging
You can use the debugger just like you used print statements earlier on. Have a copy of
your source code handy, and compile without optimisation (otherwise the line numbers
may not match the code) and with debug symbols. Start the debugger.
Work out where you want to see variable values. Set breakpoints on each line you
identify, using b {linenum} (short form) or break {linenum} (long) Now run the code.
It will continue to the first breakpoint and then stop, printing the line to be executed
next. You can now print variables by name using print or p and the variable name.
Alternately you can show all local variables with the command info locals. Continue
to the next breakpoint with continue. When the program reaches its end, you can
start again by using run. In simple cases, this may be all you need to find a problem.
In more complicated cases, like code that loops, or a fault only for some input values,
you can use conditional breakpoints. These can contain (almost) any valid expression
using named variables from your code. For example, you can check for a function
argument being > 0 with a line like
1 (gdb) break { linenum } i f { argname } > 0
You can also set breakpoints to fire on entry to a function by name rather than line-
number, such as
1 (gdb) break { m y f u n c t i o n } i f { c o n d i t i o n }
You can remove breakpoints with clear and the linenumber or function name you
used to set them. Alternately, each breakpoint is numbered, so you see output like
3
On some systems, including OSX, you may be asked for your password to allow gdb to “attach” to
a process. This is because you can attach gdb to running processes and use it to inspect their memory,
so it does need some privileges.
3.2. SYMBOLIC DEBUGGERS 57
The
1 delete { breakpoint num }
command lets you delete by number, which is useful if you have two on one line, for
example. Finally, you exit the debugger with quit. If the program is still running, you
may be prompted whether you really want to exit.
Level 0 is our current level, and in this case we are 2 function calls down from “main”
in a function called “push particles”. Note that this shows the line-numbers for each
call.
In the debugger we can step back up the frames to see how we got here. For example
we may want to examine some variables in the calling function to work out how we
got a NaN, or a negative value. This uses the frame {frame num} command or the
up {increment} command which moves us increment levels up. down moves us back
down (towards 0).
Note that if a function was inlined, no stack frame is created, and the function isn’t
shown in the backtrace. This is one more reason to debug at optimisation O0.
A Typical Session
A typical debugging session might look like this:
1 >>gdb . / eg1
2 (gdb) run
3 Program r e c e i v e d s i g n a l SIGSEGV , Segmentation f a u l t .
4 15 p r i n t f (”%d\n ” , ∗ p t r ) ;
5 (gdb) break 15
6 (gdb) run
7 The program b e i n g debugged has been s t a r t e d a l r e a d y .
8 S t a r t i t from t h e b e g i n n i n g ? ( y o r n ) y
4
A stack is a particular data structure which is “Last In First Out” so elements are taken off in
reverse of the order they were put on, like a stack of real objects. Compare a “queue” which, like a
real queue is FIFO: the first person to queue up is the first to be served.
58 CHAPTER 3. TOOLS FOR TESTING AND DEBUGGING
This value is clearly bad. We might be lucky and the value was set in only one place so
we know the error already. If we’re less lucky we’ll now have to try and trace the error.
If you work with parallel codes or on a system with a queueing system you may need
to be able to “attach” GDB or similar tools to a process that is already running. This
is not difficult, but can be fiddly. First, you need to get the process-id or PID (using
top, ps or similar). Then you “attach” to this using either gdb -p PID or gdb attach
PID ( see e.g. ftp://ftp.gnu.org/old-gnu/Manuals/gdb/html_node/gdb_22.html).
Note that GDB needs to be able to find the program executable to work. Then you
debug as normal.
5
In languages with a garbage collector the problems are not gone but are different
6
http://valgrind.org/docs/manual/manual.html
7
From the previous link: “Your program will run much slower (eg. 20 to 30 times) than normal,
and use a lot more memory.”
3.3. MEMORY CHECKING - VALGRIND 59
The heap is where any dynamically created variables are kept, that can be allocated
and freed as your program runs. The first part, lines 1-3 summarises everything this
program did with this memory. Next is a report of lost memory, that where a pointer
or reference to it no longer exists. This is C code using malloc (see line 6) in which I
created an array of length 10 (4 bytes per integer, 40 bytes total) 5 times and then lost
the pointers. Line 5 tells me this; lines 6-8 tell me (roughly) where it happened.
Lines 10-15 are a report of all possible memory leaks. First, the definitely lost
memory, to which we no longer have any pointer. “Indirectly lost” and “possibly lost”
are likely to go away once you fix all “definitely lost” memory, so focus on that first.
For details see http://valgrind.org/docs/manual/faq.html#faq.deflost. “Still
reachable” is all memory in use when your program ends; the program still has references
to it, so it could be explicitly freed, but wasn’t. It’s generally good practise to clean
8
http://valgrind.org/docs/manual/QuickStart.html
9
You don’t need the program to crash to diagnose it, and optimisation can cause reported line
numbers to be wrong. Also, high optimisation can lead to false positive results.
60 CHAPTER 3. TOOLS FOR TESTING AND DEBUGGING
up: everything is freed when your program finishes, but one day you may make changes
that turn these into actual leaks. “Suppressed” leaks are described in Sec 3.3.4.
Finally we have the summary of how many errors. Here we have 1 errors from 1
contexts. Contexts is roughly the number of different errors (type, code location etc)
and the number is the total occurrences.
Since we misused this value in a “print” we also get (in C code at least)
1 S y s c a l l param w r i t e ( count ) c o n t a i n s u n i n i t i a l i s e d byte ( s )
2 S y s c a l l param w r i t e ( buf ) p o i n t s t o u n i n i t i a l i s e d byte ( s )
which tells us the call to the system printing library has a bad count and a bad buffer.
This isn’t inside our code, but is due to it. In general, you can get a lot of opaque errors
if you send uninitialised values into libraries, especially ones like print, so it can be a
good idea to alter the code to do something (remember if you’re not using the value
nothing will be reported) less complex. In this case, the print gives us 715 errors
from 84 contexts as opposed to 8 errors from 2 contexts with a simpler use.
Here we wrote an integer (4 bytes) to the wrong memory. The second part tells us
where: 0 bytes past the end of a block of size 40 bytes we have allocated. I.e. we have
3.4. PROFILING AND PROFILING TOOLS 61
Here we read from our array after we had called free (DEALLOCATE in Fortran). The
second message tells us this was element 5 (5 * 4 bytes = 20 bytes) of our 10 element
array.
Finally there’s the one we saw above which tells us that we lost memory
1 200 b y t e s i n 5 b l o c k s a r e d e f i n i t e l y l o s t i n l o s s r e c o r d 51 o f 80
2 a t 0 x10000859B : m a l l o c ( . . . )
3 by 0 x100000E3B : f i l l a r r a y ( ∗ ∗ ∗ . c : 3 3 )
4 by 0x100000DEC : main ( ∗ ∗ ∗ . c : 2 3 )
3.3.4 Suppressions
Valgrind, particularly on OSX or when using system libraries or libraries like MPI
can find a lot of false positive errors. String buffers and padding are a common
source of these. Valgrind comes with a default set of errors it should suppress because
they’re not caused by your code, and can’t be fixed. You can read more about these
at http://valgrind.org/docs/manual/manual-core.html#manual-core.suppress.
You can safely ignore the line about suppressed errors in the output. You may some-
times want to hunt for or create a suitable suppression file for a library you’re using to
separate your important errors from the others.
When your code works, and not before, you can consider optimising.
Before doing this, you need to know which parts are slow. Often a piece of
62 CHAPTER 3. TOOLS FOR TESTING AND DEBUGGING
code will have only a few bottlenecks, or rate-determining steps, where it spends the
majority of calculation time. The quote above is worth reading carefully, as it tells
us both critical points. Also keep in mind the 80-20 rule: you often get 80% of the
result from 20% of the work, but it will take a lot longer to get the final 20% benefit.
Often this is not worth it. Finally though, make sure to distinguish good design
from premature optimisation: don’t choose approaches that wont ever be
fast enough to be useful.
3.4.1 Callgrind
Valgrind also provides a basic profiling tool, called callgrind. This tells you how many
times each function in your program was called, but note it doesn’t tell you about time
spent. Example output from a simple test program10 is something like:
1 214 ,195 ,714 Prof eg . c : solve array [ . / prof ]
2 110 ,000 ,000 Prof eg . c : add to element [ . / prof ]
3 6 ,568 ,122 Prof eg . c : divide element [ . / prof ]
4 325 ,691 ? ? ? : v f p r i n t f [ / u s r / l i b / system / l i b s y s t e m c . d y l i b ]
5 281 ,049 Prof eg . c : f i l l a r r a y [ . / prof ]
6 210 ,000 ? ? ? : rand [ / u s r / l i b / system / l i b s y s t e m c . d y l i b ]
This program creates and fills an array with random numbers, then calls a function
called solve array. This does a lot of add calls and a smaller number of divide calls.
It also does some printing.
In this case there is nothing pathological in our code, although it strongly implies
any optimisation effort would start with add to element as this is called by far the
most. However that doesn’t always mean it will take longest.
Using callgrind is quite simple. Once again, compile with -g and no optimisation
and then run the program with valgrind --tool=callgrind ./{program name} This
creates a file called callgrind.out.{pid} The pid will be printed when valgrind finishes
running, so make a note of it. Now, you run callgrind annotate {filename} on the
generated file to get output like above.
You can also have callgrind show you which function called which using --tree=caller
to get more information. Full details are at http://valgrind.org/docs/manual/cl-
manual.html#cl-manual.callgrind_annotate-options
1 Flat p r o f i l e :
2
3 Each sample c o u n t s a s 0 . 0 1 s e c o n d s .
4 % cumulative self self total
5 time seconds seconds c a l l s us / c a l l us / c a l l name
6 67.87 3.28 3.28 10000 327.82 485.70 solve array
7 29.04 4.68 1 . 4 0 1000000000 0.00 0.00 add to element
8 3.64 4.86 0 . 1 8 58908370 0.00 0.00 divide element
9 0.00 4.86 0.00 1 0.00 0.00 fill array
Comparing index 3 and 4 in this, we see that our addition is called here 1000000000
times for a total of 1.4 s ( ' 1.4 ns per call) while our division is called only 58908370
times for a total of 0.18 s (' 3.1 ns per call). Note this is without optimisation: in
this simple program optimisation implies inlining our add and divide functions for a
roughly 50% speedup without us doing anything more.
On OSX the easiest option is often to use Activity Monitor, or rather the OSX
sampler. Have your program run in a loop so that you have time to catch it.
Either open Activity Monitor, find your process by name, click the Gear icon 3rd across
in the top left of the window, click “Sample Process” and wait.
Or type sample {process name} and wait.
Example output on the same code as above (hex sample ids omitted):
1 C a l l graph :
2 2212 Thread 3743037 DispatchQueue 1 : com . a p p l e . main−t h r e a d ( serial
)
3 2212 s t a r t ( i n l i b d y l d . d y l i b ) + 1 [ 0 x 7 f f f 8 c 3 b 2 5 c 9 ]
4 2192 main ( i n p r o f )
5 + 1504 s o l v e a r r a y ( i n p r o f )
64 CHAPTER 3. TOOLS FOR TESTING AND DEBUGGING
6 + 561 s o l v e a r r a y ( i n p r o f )
7 + ! 561 a d d t o e l e m e n t ( i n p r o f )
8 + 127 s o l v e a r r a y ( i n p r o f )
9 + 127 d i v i d e e l e m e n t ( i n p r o f )
10 20 main ( i n p r o f )
11 18 p r i n t f ( i n l i b s y s t e m c . d y l i b )
12 ! 17 v f p r i n t f l ( i n l i b s y s t e m c . d y l i b )
13 . . . . ( more p r i n t f system c a l l s h e r e )
Times are in ms. Here we see that add to element takes 0.561 s whereas divide element
takes 0.127 s Since we know the former is called a lot more often than the latter, we
can see that divide is far more costly, so perhaps we have more to gain by improving
that.
3.4.3 Overheads
The other major consideration to keep in mind for optimisation is the overheads of
various elements of a program. Here we limit to just a few items in cursory form.
Function Calls
As the example above showed quite dramatically, function inlining can give significant
speedups. Sometimes however, especially in interpreted languages, this can’t be done,
and you can’t force the computer to do it. Usually the speed cost is irrelevant, but if
you divide your program into very many functions, it might start to cost you. Once
again, when the program works and not before, consider inlining the worst functions
yourself.
Useful Numbers
Some numbers to keep in mind, and a fascinating history of them since 1990 are available
at https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html
3.5.3 Fortran
In Fortran, the best option is the Nasa created PFUnit. Originally created in 2005, this
is still seeing some development. See:
https://en.wikipedia.org/wiki/PFUnit
Docs and link to code: http://pfunit.sourceforge.net/
Tutorial: https://sea.ucar.edu/sites/default/files/pFUnitTutorial.pdf
3.5.4 C
In plain C there are several options, many listed in the wikipedia link above.
We suggest Check
66 CHAPTER 3. TOOLS FOR TESTING AND DEBUGGING
Alternatively you can use CppUnit (see C++ section) with plain C.
3.5.5 C++
In C++ you’ll want a framework that can understand classes and inheritance.
3.5.6 Python
Python has several frameworks built into it, including
PyUnit: https://wiki.python.org/moin/PyUnit
Nose: http://nose.readthedocs.io/en/latest/
Doctest: https://docs.python.org/2/library/doctest.html
• Aim for broad coverage at first rather than exhaustive testing of a few functions
• Keep tests simple. You can’t test the test code, so try to make it obviously correct
Remember, you almost never have as much time to test things as you’d like, so focus
on the parts that are likely to break!
call graph A tree showing which functions call, and are called by, others. 63
call stack The chain of function calls made by your program. A stack is a particular
data structure which is “Last In First Out” so elements are taken off in reverse
of the order they were put on, like a stack of real objects. Compare a “queue”
which, like a real queue is FIFO: the first person to queue up is the first to be
served. Each level of the call stack is called a stack frame. 57, 67, 68
12
Strictly we describe only part of this philosophy here
68 Glossary - Testing and Debugging 2
continuous integration The process of continually combining code from multiple de-
velopers, usually with some automatic testing process which disallows contribut-
ing code that doesn’t work. 67
debug symbols During compilation, the variable and function names you used are
replaced with internal symbols to allow the compiler to link together all of their
occurrences. For debugging, you can attach extra information, such as the name
used in the source, the filename and line number where the definition was made
etc. This information is used by symbolic debugger to map between your source
code and the actual running executable. 56, 68
entry point (function) The start of a function. In theory, and in some old code, you
could jump into your function somewhere other than the start. This is generally
accepted to have been a Bad Idea. Note that you can generally return from a
function in several places, but be sensible.
inlining To call a function, a program will construct a stack frame, copy (where nec-
essary) variables into place in it, jump to the function code, run the content and
then jump back to the calling point, copying the return value into place if needed.
To avoid this, the compiler may inline a function instead, replacing the function
call with the content of the function. 55, 63, 64
profiling Analysing where code spends its time, to identify performance bottlenecks
and hotspots for optimization. 47, 54, 103
stack frame A single level of the call stack of your program, each frame contains
information on the function called, its parameters and in particular where the
computer should go to when the function ends.. 67, 68
symbolic debugger A debugger relying on debug symbols to allow you to run your
code and to link it to the source you wrote. For example, you can print the value
of a variable by name, even though that name may have been altered (mangled)
in the executable. Also, you can set things like breakpoints at specific points in
the source code, or you can ask the debugger to continue until the next function
call etc. 39, 55, 68
test runner Processes (threads) that can run tests etc. Usually these are idling until
they’re called upon. Automated testing systems need runners to perform tests.
67
Chapter 4
For a few files, a few libraries and a few compiler flags this is fine. But as the list grows
it becomes easy to forget or misspell files, or forget to link a crucial library. It is also
irritating to have to recompile everything every time, even though only a few files have
changed, and it is impossible to run the build in parallel.
69
70 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
This first configures the build for your particular machine and operating system, check-
ing, for example, the size of an integer, and the availabliity of certain libraries etc, and
creates a suitable “Makefile”. The next line compiles the code, and the last line installs
it, usually by copying the executable into the right place and telling the system where
it is.
Make is responsible for the compile step here. A different tool, Gnu Autotools
provides the configure step, checking for the name of the compiler and such things.
Finally the last line uses Make, but probably only to call some shell or other commands
that do the actual installation tasks.
Makefiles
To build codes using Make, you first create a Makefile.2 In the previous subsection,
Autotools was used to do this, but you will probably want to create your files by hand.
The basic idea of Make is to build targets using recipes, based on the modification
state of their prerequisites(see also dependency). A Make rule contains several sections:
• target - something to be made. For example a file, such as a .o file produced from
a .f90 or .c file, an action, such as the install action above.
• prerequisites - things that need to be made before this target can be. Can be
other targets, or can be files.
To tell Make to build a particular target, you type “make [target name]”. Make
checks all of the target’s prerequisites. If any of these need to be rebuilt, it builds them.
When this is done, it runs the recipes for the requested target.
Note the obvious property of this: if you were to make a target its own prerequisite
you have an infinite loop. This pattern, circular dependency, must be avoided.
While Make will warn you so nothing bad happens, you definitely wont get the intended
result.
Make decides whether a target needs rebuilding based on modification times. For
targets and prerequisites which are files, it checks when they last changed on disk, and
if this is newer than the target’s last modification, the target is rebuilt. For non-file
targets there are some subtleties, discussed in Sec 4.1.3.
Rules
Rules are the blocks in the file which look like
1 Target : p re re qu is i te s
2 recipe
The first rule is special and is called the default, and is invoked if you type simply make.
Otherwise you can invoke a specific rule using make {target name}
Recipes
Make recipe lines must be indented using a TAB character. Spaces will not
do. If you forget a tab, or use an editor which swaps them to spaces, you will see
something like
Note that the “6” is the line number where the error occured.
1 code : t e s t . o
2 c c −o code t e s t . o
3 test . o : test . c
4 c c −c t e s t . c
The first, default rule, has one prerequisite, “test.o” and builds the final program. The
second says how to build “test.o”, in this case from a single file, “test.c”.
Even this simple file unpacks to quite a complicated process. When we type make,
the first rule is invoked by default. This finds that it must first build “test.o”. Make
jumps to the rule for “test.o”. This depends on “test.c”, which is not a target, just a
file. So Make checks whether “test.o” needs rebuilding, and does so if necessary. The
it goes back up to the code rule, and rebuilds this if necessary.
If we run “make” once, and then run it again, we see things like
make: ‘target’ is up to date.
make: Nothing to be done for ‘target’.
This is great. We know our code has recompiled, and for large codes we don’t waste
any time rebuilding unnecessarily.
Variables
The simple compilation rules above are useful, but for more interesting tasks you’ll
want to use variables in your makefile. For example, suppose all your source files are in
a directory “src”: you could type this in every rule but you risk spelling mistakes, and
it becomes a lot of work to rename the directory.
Variables in Make are set and used like
1 variable = value
2 $ ( v a r i a b l e ) #Use v a r i a b l e .
The dollar indicates this is a variable, while the brackets allow the name to be more
than a single character. If you forget the brackets you wont get a helpful error. Instead
the line will be interpreted as a single-letter variable followed by the rest of the variable
name. Remember the brackets around Makefile variables.
Because of the nature of makefile commands, the file is read multiple times, and
then any rules are executed, so some things can appear to happen out of order. This
allows the system to be very powerful. For the current purposes all we need to worry
about is variable assignments. Makefile variable assignments have one major difference
to those in other programming languages. The normal “=” operator sets two variables
to be the same3 , like this
1 var1 = test
2 var2 = $ ( var1 )
3 var1 = test2
4 $( info $ ( var2 ) ) #P r i n t t h e v a r i a b l e . −> t e s t 2
3
i.e. var2 becomes a reference to var1
4.1. BUILD SYSTEMS 73
The “:=” operator does the assignment using the value right now4
1 var1 = t e s t
2 var2 := $ ( var1 )
3 var1 = t e s t 2
4 $ ( i n f o $ ( var2 ) ) #P r i n t t h e v a r i a b l e . −> t e s t
Note also that we don’t use quotes on the strings here. Make treats quote marks like
any other characters, so they would just becomes part of the string.
Make also provides a selection of automatic variables for use in rules. These give
you access to things like the target being built and the prerequisites. In particular we
have
1 $@ # t a r g e t o f t h e c u r r e n t r u l e
2 $< #f i r s t p r e r e q u i s i t e o f c u r r r e n t r u l e
3 $ˆ # space separated l i s t o f a l l p r e r e q u i s i t e s
For example this snippet shows the use of these by setting the recipe to write to the
shell. The “@” character here tells make not to print the command it is about to run.
1 other : f i n a l
2 @echo $@
3 final :
4 echo $@
5 t es t : other f i n a l
6 @echo $@ ’ | ’ $< ’ | ’ $ ˆ
Implicit Rules
Make has one final internal variable that is very useful, which is the rule wildcard. The
“%” stands for any character sequence, but anywhere it appears in the rule it is the
same sequence. This is used in what are called implicit rules, for “implicit” targets, i.e.
those that aren’t specifically mentioned in the makefile. These are also called pattern
rules, because they apply to targets matching a pattern.
4
i.e. var2 becomes a copy of var1
74 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
These rules match any target that looks like “[name].o” and build them from the cor-
responding “[name].c” or “[name].f90”.
A full discussion of pattern rules is at https://www.gnu.org/software/make/
manual/html_node/Pattern-Rules.html. Note that Make has a lot of built-in rules
for the normal procedure for a given language, summarised at https://www.gnu.org/
software/make/manual/html_node/Catalogue-of-Rules.html#Catalogue-of-Rules.
Multiple Rules
You may notice that the pattern rules above are perfect for generating a .o from a single
.c or .f90, but don’t allow you to specify more dependencies for your code. For example,
in c %.o would usually depend on %.c and %.h. Or you may have some helper functions
in a file which all your other files depend on. Make allows you to define multiple rules
for the same target. Only the last recipe is run, but prerequisites from all rules
are considered. This allows us to use the pattern rules like above, and also, elsewhere
in the file, specify our file dependencies. See the example in Sec 4.1.3.5
Other Bits
Make has many other features to aid you in processing filenames, putting things in the
right directories, and controlling your compilation by passing variables to make itself.
We are not going to go into the details here, as there are many good tutorials out there,
and you are best served learning the details when you actually need them. However,
one last feature of Make is very common and quite important, which is using Make for
a target which is not a file.
As we saw above, Make can run any command you wish, and targets need not
correspond to a filename. These targets are always rebuilt, as Make has no way of
tracking their last modification. A common use of this is to have a “clean” target,
which cleans up intermediate and output files. This is fine, unless a file ever exists
which is called “clean”, when make will think this target is up-to-date and do nothing.
To avoid this, and to make things clearer, make has a special target, .PHONY. Any
prereqs of this special target are assumed to be phony, i.e. not to correspond to any
file.
1 # S e t c o m p i l e r name
2 CC = c c
3
4 #S e t phony t a r g e t s
5 .PHONY : c l e a n
6
7 #D e f a u l t r u l e
8 code : t e s t . o
9 $ (CC) −ocode t e s t . o
10 #P a t t e r n f o r c o m p i l i n g . c f i l e s
11 %.o : %. c
12 $ (CC) −c $<
13
14 #L i s t t h e d e p e n d e n c i e s o f t e s t . o h e r e
15 #I t w i l l be b u i l t with t h e r u l e above
16 test . o : test . c
17 #Clean r u l e
18 clean :
19 @rm − r f code
20 @rm − r f ∗ . o
Parallel Building
The simple build scripts tend to use a single line to compile everything and then link
in a single step. With Make, you instead give it dependencies, so it knows the order
in which it needs to build files. This means it can work out which rules can be built
simultaneously, and parallelise your building. Using make -j {n procs} Make will use
up to n procs, but only as many as it can. This can speed things up a lot.
Pitfalls
Make is very powerful but it does have some traps that are easy to fall into.
• Forgotten dependencies
Target wont rebuild when it needs to
Can be very hard to diagnose
Your compiler can often generate dependencies (see gcc -M)
• Circular dependencies
Make will ignore these
Might create a forgotten dependency
• Permanent rebuilding
Non-file targets always rebuild
Any rule that doesn’t actually build the target file can too
76 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
• Overcomplication
6
“Python” generally refers to CPython, a particular variant of Python. Others exist, such as
IronPython and PyPy, and not all features work exactly the same in all of them.
4.2. DISTRIBUTION SYSTEMS 77
• DEB (Debian based packages, for systems include Ubuntu, Mint) https://wiki.
debian.org/Packaging/Intro
Simple Cases
For fairly small projects you can simply distribute your source code, using tarballs
(zips) or a public repository, and provide a list of dependencies (or a simple script to
obtain them) and a simple makefile with a few conditions. For example, you can write
a makefile to accomodate different compilers, which expect different flags, by providing
arguments in your make file like “make COMPILER=intel”. As things get a little more
complex you may turn to cmake, or qmake rather than hand-create a complex makefile.
A user downloads your code, installs the needed libraries, and builds the code.
Intermediate Cases
In more complex cases, where you may wish to accomodate a lot of details of com-
pilers, or choose between different libraries or even make them optional (for exam-
ple, many programs that can work with PS files will use GhostScript if available, but
have fallbacks otherwise), handwritten makefiles get too complicated. In this case
you can turn to something like Autotools (http://inti.sourceforge.net/tutorial/
libinti/autotoolsproject.html) to create a Makefile for a particular installation.
This can also create an include file for your code giving it access to information like
Integer sizes. With Autotools you have the (possibly familiar) sequence
1 . / c o n f i g u r e −−o p t i o n s=xyz
2 make
3 make i n s t a l l
Difficult Cases
For more systems even more complex than this, you will have to look at containerisation
systems. Note that the previous setup is enough even for major endeavours such as
BLAS or SageMath. These distribute an entire operating system and the installed
software, which requires setting up disk access in and out of the container, network
drivers and interconnects in HPC. Options include
• Virtual Machines (many options, Virtual Box etc)
• Docker (https://www.docker.com)
• Singularity (http://singularity.lbl.gov)
• Shifter (https://github.com/NERSC/shifter)
the last two of which are designed with HPC in mind.
While the basic functions are quite similar in all VCSs the more complex features
often differ quite a lot. The terminology often differs too. The most likely system you’ll
be using is “git”10 , so that is the one we are going to talk about here. Note that it is
not the only good option. You’re also likely to use some sort of online service, likely
“github”11 . Alternately, the Warwick SCRTP has an online system.12
• Mercurial: https://www.mercurial-scm.org
• Subversion: still around, needs a server running, but that can be on the local
machine https://subversion.apache.org/
edition. Git shares many of the useful features developed by earlier version-control
systems. In particular:
• Moved/renamed/copied/deleted files retain version history
• Sophisticated branching and merging system (see Secs 4.4.3 and 4.4.4 for details)
• Used a distributed storage system, where each developer has as much of the
repository as wanted in a local copy and merges onto central server when ready
Note that Git is not Github, and Github is not Git. Github is one popular
online host of git repositorys but it has its own model for how to work and adds features
like issue-trackers.
However, you can use several different email addresses, for example for work and for
personal projects. In this case, after “git init” but before anything else, you should
1 g i t c o n f i g u s e r . name ” John Doe”
2 g i t c o n f i g u s e r . e m a i l johndoe@example . com
Now you can add files to the repo. You usually do this in two steps. First you add, or
stage the change, that is get things ready, and then you commit. You can add multiple
files, or parts of files, before carrying on.
1 g i t add s r c /
2 g i t commit
13
If you copy and paste these, note that before “global” should be two hyphens
82 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
The second line results in a text editor opening to allow you to specify the “commit
message” to explain what and why you are adding. The editor can be changed14 and
often defaults to vim or nano.
Git commit messages should follow a particular format, which originates from its
use controlling the code of the Linux Kernel.15 A typical message looks like
First check in of wave.f90
wave.f90 will be a demo of using a ‘‘wave’’ type MPI cyclic transfer 0− >
1− > 2 etc. in order.
The first line is the subject, and should generally be less than 50 characters. The second
line must be blank. Any text here is ignored. The subsequent lines are the message
body, and should generally be less than 72 characters. You can use as many lines as
you like, but be concise.
You now save and exit the editor, and git gives a short summary of what was
committed. If you quit without saving the commit is aborted. The state of the files
we committed has now been saved. Now we can make some changes to the files, and
commit those. If we just try
1 g i t commit
1 git status
which tells us the current state of the working directory: which files have changes that
have been added, which have unstaged changes, and which files are not included in the
repository. If the last list is very long, you may want to use a .gitignore file to tell
git to ignore some file types. See e.g. https://git-scm.com/docs/gitignore
There are two useful shortcuts: for a few files that have been previously added so
are known to git, we can explicitly commit them, without an add step like
1 g i t commit f i l e 1 . t x t f i l e 2 . t x t
1 g i t commit −a
In all cases, we get the editor, we write a useful commit message and then we get some
report like 1 file changed, 2 insertions, 3 deletions
We can see all of the commits we have made using the log.
1 git log
gives us output like Fig 4.2 Note the string after the word “commit”. This is the
“commit id” which uniquely identifies the commit. Git also accepts a shorter form of
this, usually the first 8 characters.16
84 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
Figure 4.3: Typical “git diff” output. A line referring to “tag” has been removed, a
line defining “dummy int” has been added.
where the lower bound is exclusive (last commit you want to leave unchanged) and the
upper bound is inclusive (last commit you want to undo). When you do this, you will
get the commit message editor for each reverted commit, saying Revert ?original
commit message?. You rarely want to change these.
4.4.3 Branching
If you are working on several things at once, you may find branches useful. These are
versions of code that git keeps separate for you, so that changes to one branch do not
affect another. Whenever you create a repository, a default “master” branch is created.
Adds and commits are always on the current branch. The command
1 g i t branch
The branch is based on the last commit (on whatever branch you are on when running
the command)18
The branch command doesn’t move you to the new branch. You do this using
1 g i t c h e c k o u t {name}
You will get a message, usually Switched to branch ’name’, or an error message. To
create a branch and change to it in a single step, use
1 g i t c h e c k o u t −b { new branch name } { e x i s t i n g b r a n c h n a m e }
where the existing branch name is optional. This is very useful when working with a
branch from a remote server, for example.
Checkout also lets you go back to some previous version of the code, and create a
branch from there using
1 g i t c h e c k o u t −b { new branch name } {commit ID}
You can checkout old versions without changing branches too, but this puts your repos-
itory into an odd state, so is best avoided for now.
Note that if you have uncommitted changes when you run git branch, those changes
will come with you, and can be committed. If you try and change branches when
17
E.g. https://stackoverflow.com/questions/31057892/i-accidentally-committed-a-
sensitive-password-into-source-control
18
You can branch from branches, and create very complex trees, but for now you will mostly want
to create branches based on master.
86 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
you have uncommitted changes, you may get an error, saying error: Your local
changes to the following files would be overwritten by checkout:. You can
either commit those changes, or consider using “git stash” to preserve them to use later.
See e.g. https://git-scm.com/docs/git-stash for the latter.
4.4.4 Merging
When you use branches to develop features, you usually eventually want to bring them
back into the main version, when they’re ready to be shared with users, or are fully
complete. This is a merge, and uses the command
1 g i t merge { o t h e r b r a n c h n a m e }
which brings changes from the other branch into the current one.
Figure 4.4: A conflicted “git merge”. non-thermal contains a change incompatible with
our current branch (labelled HEAD as we’re currently on it)
If you’re lucky, the merge will be automatic and you will see a message about
Fast-forward and are done. Otherwise, you will end up with files containing markers
using the git diff format. Figure 4.4 shows an example. You will have to go through
each file and “resolve the conflicts” (fix what git didn’t know how to merge) before git
lets you commit them. When you are done, finish using
1 g i t commit #As normal
2 g i t merge −−c o n t i n u e #A l t e r n a t i v e i n newer g i t v e r s i o n s
There are tools to help with merges, but they can get quite complicated, and while
git tries to understand the language, it is a difficult problem in general. For example,
if you have changed the indentation of a whole block of code, you may see the entire
thing being removed and added again, and showing as a merge conflict.
4.4. BASIC VERSION CONTROL WITH GIT 87
Fig 4.5 shows a typical flow of branching and merging. When feature 1 is complete,
it is merged back to master, and the feature 2 branch pulls in those changes to stay
up-to-date, before continuing work. When feature 2 is finished, it is merged too.
Commit
Commit
Commit
Feature 1
Branch Merge
Master
Branch Merge
Commit
Commit
Commit
Commit
Merge
Feature 2
Figure 4.5: A schematic of typical git workflow with two feature branches and one
master.
Git is a distributed, networked version control system, which is the core of its real
power. You can link between a local repository and a remote one, on a server, or on
e.g. Github, and git remembers that. You can clone code from a remote repository and
git will remember the origin. To clone code, you need the url of a remote server, then
use the command
1 git clone { url }
Fig 4.6 shows an example using github - here you can get the url showing the green
“clone or download” button on the repo’s page. This completely sets up the repo, and
stores the “remote tracking” information (mostly the url you used). Note this will be
a subdirectory of where you ran the command.
1 g i t branch −a
88 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
will tell you about all branches, including those on the remote you now have references
to. Your master will now be linked to a remote master branch. The other branches are
not downloaded by default, so if you check them out you will see similar text to Fig 4.6
about counting and receiving.
This happens on a per-branch basis. Note that there is a related command, fetch, which
just updates branch information and downloads changes, but doesn’t merge them into
yours. If your local copy has also changed, you will have to deal with merging changes
from other developers with your own.
To upload your changes to the remote, you can push them, using
1 g i t push
If you are working with somebody else’s repository, check whether they
allow you to push directly. On e.g. Github, a different model is used, see
Sec 4.4.7 Git tries to merge your changes with the remote copy, so make sure to pull
first, or it will fail.
19
To allow your users to tell you about bugs, requests etc.
4.5. RELEASES AND VERSIONING 89
Commit
Commit
Commit
Feature
Branch
Master
Pull request
Master
Figure 4.7: A schematic of typical Github workflow. You develop a feature on your
fork, and them submit a pull request to have it included in the main repository.
• 2.x.y is a major version. This usually introduces significant new features beyond
those of version 1.p.q. Sometimes it means old behaviour has been removed,
changed, or otherwise broken.
• x.2.y is a minor version. This usually adds smaller new features, adds new be-
haviour without breaking the old, etc.
• x.y.2 has many meanings. It may be called patch, build, release or otherwise,
and usually starts with a number. Sometimes this is followed by a dash and an
90 CHAPTER 4. WORKFLOW AND DISTRIBUTION TOOLS
The latter stores your name as creator and has various other advantages, such as ability
to specify a message, so is generally recommended. Using the 3-part system we may do
something like
1 g i t t a g −a v0 . 0 . 1 −m ” P r o t o t y p e v e r s i o n ”
We can then find out about the tag and the commit is is attached to using
1 g i t show v0 . 0 . 1
to share them.
The major advantage of this is that you can then include some recipe in your makefile
which can extract the version information and pass it on to your code. Because git tags
20
Note that often backwards compatibility is achieved by keeping the old code, and having the
system use it when given an old file, but this does often mean code duplication.
Glossary - Workflow and Distribution 91
are separate to your source code, you can go back and do this after you have committed
changes. The code to extract the information is a bit horrible, but for example, if you
are sure your code will only be obtained via git (and not e.g. downloaded, in which
case you’ll need something more complex) you add the following to the preamble part
of your makefile
1 GIT VERSION := $ ( s h e l l g i t d e s c r i b e −−abbrev=4 −−d i r t y −−always −−t a g s )
2 CFLAGS += −DVERSION=\”$ (GIT VERSION) \”
branch Repositories can have more than one branch, an independent set of changes,
often for some purpose such as a specific feature or fix. Branches can be merged
together to combine their changes. 79, 92
commit (A commit) A chunk of changes, usually along with a message and an author
etc. (To commit) To record files or changes in the version-control system, i.e. to
add its existence and state to the history and store files and content however the
system decides (often as incremental diffs). 81, 92, 93
dependency In general terms dependencies are the libraries, tools or other code which
something uses (depends on). In build tools specifically, they are also known as
prerequisites and are the things which must be built or done before a given item
can be built or done. For example I may have a file-io module which I have to
build before I can build my main code. 70
fork To split off a copy of code to work on independently. Often this implies some sort
of difference of opinion as to how things should be done. In the Github model,
forks are encouraged in normal development: one forks code, adds features and
then may or may not make a pull request back to the original repository. This
92 Glossary - Workflow and Distribution
way only core developers can commit to the main repository, but anybody can
easily modify the code. 88, 92
forwards compatibility A guarantee that files etc from a newer code version will
still work with the older version, although some features may be missing. E.g. a
file format designed to be extended: extended behaviour will be missing, but will
simply be ignored by old versions. See also backwards compatibility & sideways
compatibility, 90
master A common name for the primary branch of a code. Master should always be
working and ready to release, as it is the default branch in git. Some consider it
a bad idea to commit changes directly to master, preferring to work on branches
and then merge or rebase.
merge Combining one set of changes with another, usually by merging two branchs
The combined version usually shares its name with the branch that has been
merged “onto”. See also rebase, 86, 88, 91, 92
pull To download changes to a repository. In git this then integrates them with your
local copy. If you have local changes, the remote changes are merged into yours.
88
pull request A request to pull-in code from elsewhere. In the Github model, one forks
code, makes changes, and then raises a pull request for them to be integrated back
to the original repo. 88, 91
push To upload your local state to a remote repository (see repository). You can push
commits, whole branches, git tags etc. 88, 90, 93
Glossary - Workflow and Distribution 93
rebase Replay changes as though they had been applied to a different starting point.
Because systems like git work in terms of changes to code, they can go back
through history and redo changes from a different base. For example, one can
“rebase” a branch onto another. The changes in the first branch are taken one by
one, and applied to the second branch. This differs from merging mainly in how
changes are interleaved in the history. See also merge,
repository (Aka repo) A single project or piece of software under version control. In
general a local repository is a working (in the sense of “to be worked on”) copy of
the code, whereas a remote repository is a copy everybody shares, pushing their
work and combining changes. The remote copy can be sitting on somebody’s
machine - remote is a designation not a requirement. Note that git does not
require a remote repo (or server), but some systems like subversion do. 81, 92
sideways compatibility A guarantee that code remains compatible with other code.
For example you may create files for another program to read, and you want to
make sure that your output remains compatible with their input requirements,
even when these may change. See also forwards compatibility & backwards com-
patibility,
stage Staging means preparing changes to be added (commited) and comes from the
similar concept in transport, https://en.wikipedia.org/wiki/Staging_area.
81, 82
VCS Version Control System; a tool for preserving versions of code and details about
who, why and when they were created. 78, 79, 80, 90
Chapter 5
Wrap Up
These notes have gone rapidly through a wide range of ideas, tools, and principles to
give you a basic toolkit for developing software. Many things were left out and glossed
over, and even the topics we have covered were necessarily fairly cursory, to make sure
you have a grasp of the breadth of the subject. We promised at the start that we would
only ever describe something as a must if it really was always the case. We gave far
more should statements. These are broadly true, but not absolutely.
All of these statements are reproduced in Appendix B. Question them. When you
can explain why we say them, and why they can break down, you know you understand.
You may have noticed a few themes running through these statements too, mainly
It’s only those who do nothing that make no mistakes. – Joseph Conrad
94
5.2. WHERE TO GO FROM HERE 95
First learn computer science and all the theory. Next develop a pro-
gramming style. Then forget all that and just hack. – George Carrette
atomicity Most code operations map into several instructions to the computer. Atom-
icity is the property that either the entire action will be performed, or none of it.
This is commonly encountered in databases: for example if you overwrite a record
with a new one you want to be sure not to end up with a mashup of the original
and new record if something goes wrong. In some cases writing to a variable is
not atomic: you could end up with one byte in memory from the old value and 3
bytes from the new, giving garbage. This is mainly a concern with multi-threaded
programming or interrupts. 12
compiler flag (Aka directive) Command line arguments passed to the compiler to
control compilation. For example in C you can define a value (for use with #ifdef
etc) using -D[arg name][= value]. Optimisation levels (how hard the compiler
works to speed up or reduce memory use of your program) are usually set with a
directive like -O[level number]. 69
heap Program memory that can be used dynamically (determined as the program
runs), for example anything used with malloc in C, ALLOCATABLEs in Fortran
etc. C.f. stack, 59
interface The functions available, including their signatures. The bare minimum
somebody would need to use a chunk of code. 14
language standard Rules specifying what valid code is in a given language, and what
must be guaranteed by a compiler or interpreter about how this is implemented.
2, 4, 25, 97, 102
mutability In languages like Python, some data types are fixed when created, and
cannot be changed later. These are called immutable. In practise you will mainly
notice this with tuples. You can create a new tuple from some values, but you
can’t change a single element. Similarly, with strings you cannot change a single
character, you have to create a new string with the change included.
Glossary - General Programming 97
pass(ed) by value Different languages pass parameters into functions differently. When
passed by value, the current value (at call time) of the variable is copied to a
dummy variable inside the function. For example a function
FUNCTION inc(x)
x = x+1
END FUNCTION
y=1
inc(y)
PRINT y
would give 1 as y is not changed by the call to inc. See also pass(ed) by reference,
5
scope Scope of a variable is the region of the program in which it exists and can
be used. Most languages have “function scope” so variables you create inside a
function can’t be used outside it. C-like languages add “block scope” so a variable
defined, for example, inside an if-block is lost when the block ends. 51
source (code) Your program text. This is distinct from the runnable executable, or
the bytecode or tokenised code produced by e.g. Python. iii
stack Program memory used for static variables (where the memory needed is known
at compile time and can’t change) such as numbers, strings etc. C.f. heap,
undefined behaviour Things which are not specified by a language standard can do
whatever they want - their behaviour is undefined. Beware that this can mean
doing exactly what you expect. Until it doesn’t. 5, 27, 30, 40, 55, 102
Appendix A
• Doxygen http://www.stack.nl/~dimitri/doxygen/
• GDB https://www.gnu.org/software/gdb/
• Valgrind http://valgrind.org/docs/manual/manual.html
• gprof https://sourceware.org/binutils/docs/gprof/
• perf https://perf.wiki.kernel.org/index.php/Main_Page
• Make https://www.gnu.org/software/make/
98
99
it public, showing lists of recent items; several allow you to make your item unlisted
but sharable via link.
Where possible below I have created an example using the IEEE floating point rules C
code and included that Sharing only:
• Pastebin https://pastebin.com/ With a free account you can control and delete
your content. Example at https://pastebin.com/Vdqmm4rE
• Github Gists https://gist.github.com/ Requires Github account. Example at
https://gist.github.com/hratcliffe/2dd02726bb18323c78d78acbd9d2c1f1
Allow running and showing results:
• CodePad http://codepad.org/ (Allows private pastes) Example at http://
codepad.org/k0q8kOef
• IDEOne https://ideone.com Example at https://ideone.com/0ryEtS Includes
Fortran support
• https://aws.amazon.com/cloud9/ Requires AWS signup, may not be free.
• For SQL database code http://sqlfiddle.com/
B.1 Must
• As researchers, your priority is research. You code needs to work, and you need
to know it works. Anything that does not enhance that goal is decoration.
• If your code involves any sensitive data, you really must read up on relevant
regulations and protocol
• Before you start coding in earnest, you will want to have some sort of plan
• if protocol matters you must find and follow the proper guidelines
• Do not optimise the algorithm at this early stage, but do not be afraid to throw
it away if it will clearly not serve your purpose.
• Self-documenting style does not mean you do not have to document your code
102
B.1. MUST 103
• If your work is funded by a research council you must read and obey any rules
they have as to sharing your code and its results
• when you have eliminated the impossible and still not found the answer, perhaps
it wasn’t as impossible as all that.
• compilers will warn you about bugs like accidental assignment instead of compar-
ison if you let them
• The fanciest debugger in the world will not help you if you are not thinking
• running tests tells you that the tests pass, not that the code is correct. You must
work hard to make this the case, not suppose it.
• Unless your requirement is bit-wise exact reproducibility, (page 46) do not com-
pare floats for exact equality in your tests.
• Once your code works, and not before, you may want to consider profiling
• Testing is a tool, not a goal in itself. Your goal is writing correct software to do
research.
• Remember that any two points can be joined by a straight line so you need at
least 3 points for convergence
• The most vital debugging and testing tool is your own brain.
• Always start at the first error! Often the rest are the same error popping up later
on.
• When your code works, and not before, you can consider optimising. Before doing
this, you need to know which parts are slow.
• Make recipe lines must be indented using a TAB character. Spaces will not do.
• Only the last recipe is run, but prerequisites from all rules are considered
• The most important thing about version control is to do it. It doesn’t matter
how, as long as it works.
104 APPENDIX B. MUST AND SHOULDS
• If you are working with somebody else’s repository, check whether they allow you
to push directly. On e.g. Github, a different model is used, see Sec 4.4.7
B.2 Should
• Divide into reasonably sized functions
• Binary bisection has uses in all sorts of areas, so make sure you understand how
it works.
• The moral here is NOT that you should not use libraries, but to be selective:
• It is always a good idea to write a simple program with a new tool, before trying
to integrate it into your actual code.
• you should help your users to preserve the information to reproduce their work
in future
• do not trust anything supplied by a user, even if that is you. This is not because
users cannot be trusted, but mistakes and ambiguities can happen.
• But if you find yourself wondering “but why” very often, consider whether you
have got the right tool for the job.
• your aim is usually to find where what you thought would happen diverges from
what actually happens
• you should consider adding code to check for violation of your limitations
• The tests you really need to run are probably the ones you haven’t thought of,
because you never realised that could be a problem!
• Be careful when setting error intervals: absolute values are useful for things like
floating point errors, but in most we probably want to use an error percentage
• Avoid bit-wise exactness testing where ever possible. In most cases it is excessive.
This means no exact comparisons of floats.
B.2. SHOULD 105
• the compiler will not reorder dependent pieces of code; it can omit unreachable
code and no-ops and can remove certain other things.
• Fix earlier errors first. Often later errors are a direct result of earlier ones, so it
pays to consider them in order.
• make sure to distinguish good design from premature optimisation: don’t choose
approaches that wont ever be fast enough to be useful.
• If you are not already, you should consider using Python modules, rather than
simple scripts.
• Avoid breaking backwards compatibility where practical. That is, older data files
should still work with newer code
• The first thing to do, once you’ve decided on a version scheme such as the 3-part
one above, is to make sure it is embedded into your output files (see also Sec
1.7.4).