• No results found

Code reviews

N/A
N/A
Protected

Academic year: 2021

Share "Code reviews"

Copied!
12
0
0

Bezig met laden.... (Bekijk nu de volledige tekst)

Hele tekst

(1)

Code reviews

Arjen Markus1 Deltares The Netherlands

Introduction

There is an extensive body of literature on the reviewing of code and design documents as a means to check that the software in question is doing what it is supposed to do and that the implementation is maintainable and understandable. Some development methodologies, e.g. Extreme Programming (cf. Beck and Andres, 2004), prescribe continuous reviews by peers (cf. P. Goodliffe, 2007). In all forms code reviews are about finding defects of one type or another in the code, they are not intended to criticise the programmer.

While the procedural (and psychological) aspects of code reviews are widely described, some practical aspects are not, for instance: what should you be looking for in the code? It is not enough to check that the code adheres to the programming standard of the project it belongs to. Such a standard may not exist, be incomplete or be focussed on layout, not on questionable constructs that are a liability. With this article I would like to fill in this practical gap, at least partly.

What do we want to achieve with code reviews? First, we want to verify that the code is doing its job and that the code is of good enough quality:

It is readable by others apart from the author

It is maintainable – adding new functionality or correcting bugs should not amount to hard labour

It is testable – do we understand what goes in and what comes out?

Secondly, the code should be portable – that is: it should be not only be possible to build the program and run it on a different operating system, but also with different compilers and different versions of the same compiler. Many compilers offer options to check against the standard – use it to see if you accidentally used some compiler-specific feature.

From these considerations we can formulate four “principles” and a number of practical guidelines: Be Explicit Don’t Stray Avoid Traps Clean Code

Be Explicit

The source code is the most important product from which we can judge the working of the software, therefore we must rely on that source code to give us all the information we need:

Use explicit declarations of variables and constants 1

(2)

While Fortran of old is perfectly happy to use undeclared variables and determine their types from a few simple rules it is also very easy to make mistakes that go undetected for a long time, if you rely on them. Consider this code fragment:

integer :: i real, dimension(10) :: x ... do i = l,10 x(i) = 2.0 * i + l enddo

The typos (the use of a lower-case “L” instead of a digit “1”) would be caught by the compiler if explicit declarations were enforced. So: always use IMPLICIT NONE.

Some compilers have an option to force an error if an undeclared variable is used but then you rely on something outside the source code and it is not a portable feature. Also, some

compilers allow you to change the meaning of a default real or integer – make reals double precision or turn integers into 8-byte integers. Again, this makes the program rely on things outside the code.

Another example: the use of literal numbers in the source code:

rate = rate * 1.15740740740741e-5

The number will probably not be recognised as 1/86400, where 86400 is the number of seconds in a day. The computation is intended to convert the “rate” from day-1 to s-1. Instead declare a parameter to make this conversion explicitly:

real, parameter :: per_day = 1.0/(24.0*3600.0) ! Convert from 1/day to 1/s

And while we are at it, use an explicit kind:

integer, parameter :: wp = kind(1.0) ! Working precision !

! An alternative that is even more precise:

!integer, parameter :: wp = selected_real_kind(6,37) ! Working precision

real(wp), parameter :: per_day = 1.0_wp /(24.0_wp * 3600.0_wp) ! Convert from 1/day to 1/s

The reason for this, though it may seem rather pedantic, is that it will be much simpler to change the precision of the program – just replace the definition of the parameter wp:

integer, parameter :: wp = kind(1.0d0) ! Working precision is double precision

Use preconditions

If a subroutine expects its arguments to conform to particular conditions, for instance: if it reads a file, that file must already exist, then check these conditions inside the routine. In a code fragment like:

inquire( file = ’myfile.inp’, exist = exists ) if ( exists ) then

call read_file( ... ) endif

the condition should probably be moved into the subroutine itself. Now you have a distributed responsibility: the calling code must make sure that the routine is not inadvertently called, whereas the routine itself must also make sure that the file can be properly read.

(3)

The issue arises in a slighlty different form with composite conditions too:

if ( allocated(array) .and. array(1) > 0 ) then ...

endif

If the array is not allocated, then the second part does not need to be evaluated, from a logical point of view, but there is no guarantee that the program will never do that: Fortran does not guarantee short-circuiting.2

So, the above should rather be written as:

if ( allocated(array) ) then if ( array(1) > 0 ) then ...

endif endif

The principle of explicitness also applies in these cases:

Variables that need to retain their values between calls

Fortran has several rules to determine if a local variable retains its value between calls. But why rely on your understanding of such rules? Use the SAVE attribute or statement wherever you want a variable to behave that way. (Worse: some compilers keep all the local variables in static memory because it is more efficient on that particular platform. However, it means that your program may work on that platform but not on others when you forget the SAVE attribute). And of course: be explicit about what variables you want to save – do not use the SAVE statement without any names.

Visibility of the interface to a subroutine or function

Modern Fortran programs will probably use assumed-shape arrays, optional arguments or derived types and other features. In these cases it is very important that the compiler “knows” about the interface for the subroutines and functions. This requirement is automatically

fulfilled if you use modules for all your routines. Do organise the modules in a comprehensive way: routines that go into one module should be somehow related, manipulate the contents of the same derived type for instance.

Availability of variables and routines

If you use variables and routines in a module, then, by default, they will be available for any program unit that uses the module. This may or may not be what you want: a program could inadvertently use a variable reserved for the inner workings of your module and cause bugs that are difficult to find. So: use the PRIVATE statement to hide any data and routines and only make those items PUBLIC that should be public. The same goes for the content of a derived type.

The default case in a select block and the else case in an if block

2

Short-circuiting can lead to inefficient machine code, especially when the condition has multiple logical connectors. The given example could actually fail at run-time as it may access an element of an unallocated array.

(4)

Quite often you will be sure that a particular condition will not occur, like in a SELECT block only certain cases will ever need to be handled. It does not hurt to insert code that makes this explicit:

select case (case_var) ... legitimate cases ...

case default

write(*,*) ’Programming error: case should not occur: ’, case_var end select

A similar thing occurs with nested if-blocks or if/elseif constructions: if you are certain a particular alternative should not occur, then say so.

Appropriate error messages

Error messages should be to the point: accurately indicate what is wrong and – if possible – what can be done about it. Code like this:

if ( n > nmax ) then

write(*,*) ’Invalid number of items’ stop

endif

is wrong and plainly unhelpful – there is nothing wrong with the number of items, except that it is larger than the maximum that is allowed (I have encountered such code in practice! Only by examining the source code was it clear what was wrong.) Use dynamically allocated memory – possibly a bit more work but more robust – so that the error condition can not occur anymore.

If that is not practical, for whatever reason, replace the error message by something more informative (maybe including the location in the source too):

if ( n > nmax ) then

write(*,*) ’Error: More items (’,n, ’) than can be handled.’

write(*,*) ’ Parameter nmax (now: ’,nmax, ’) must be increased to at least ’, n stop

endif

This alternative gives a clear indication of what is wrong and what can be done about it. In summary:

Use IMPLICIT NONE, use parameters instead of literal constants, use parameters to specify the numerical kind

Explicitly check the input arguments: do they satisfy the preconditions of the routine? Put these checks inside the routine.

Watch out for composite logical conditions: do not rely on short-circuiting Use SAVE for local variables that should retain their value

Use modules for all your code

Use PRIVATE and PUBLIC to control the access

Provide error messages that inform the user about what to do

Of course there are always good reasons why it is impossible or undesirable to conform to these guidelines. For instance: routines that should be called by a program in another language should most probably not be in a module. But if you have such reasons: be explicit about them!

(5)

Don’t stray

Many projects use a programming standard and guidelines for coding besides the standard of the programming language itself. Published examples include Boukabara and van Delst (2007) and Kleb and others (2009). They represent the practical experience people have built up using the language. This may include typical solutions for common programming tasks – the idiom – and choices for the layout of the code (indentation for instance). Don’t go against the grain. Even if the standard or the idiom prescribed by the project is silly, use it anyway:

If you modify a program’s code, use the same layout and programming style. Code that looks like a hotchpotch of styles is not very appealing. If it is already bad, advise to clean it up first.

Conform to the agreed standard. If you think the standard should be changed, discuss it separately from work on the code.3

That also means that the programmer should have a good understanding of the language: what features are acceptable and what features are a possibly common but nevertheless

non-portable extension? The programmer should also be aware of things that are explicitly left to the discretion of the language implementation.

Here are a few examples:

Declarations of the type REAL*4 or REAL(4) are not portable and should be removed. REAL*4 is common extension but it has never been part of a Fortran standard. REAL(4) hints at a common misunderstanding of the KIND mechanism: compilers are free to use any positive integer number to select the precision and range of the integer and real types. This is not necessarily identical to the number of bytes these types occupy in memory.

Unformatted files are not portable between platforms: the record administration may differ, though there exists a popular structure that is (almost) universal. Besides the record structure the ordering of the bytes that make up a single number may differ, as well as the interpretation of these bytes. Again, a ubiquitous ordering and

interpretation exist, but they are not universal.

Direct access files pose a peculiar issue: the unit of length is one byte with some compilers and one word (4 bytes on a 32-bits platform) with others.

Tab characters have no place in Fortran source code (with the possible exception of literal strings). They are not part of the Fortran character set. Simply do not use them. In the editor they may expand to the right number of positions, but on a print-out the resulting indentation can be horrible.

A code fragment like:

if ( x > 0 ) then valid = .true. else

valid = .false. endif

indicates the programmer does not quite understand logical expressions:

valid = x > 0

is a simpler equivalent. Of course:

if ( x > 0 ) then

3

Few things seem to be so provocative as posing a standard to programmers (cf. L.Hatton, 1995 or c2.com/cgi/wiki/CodingStandard).

(6)

valid = .true. endif

is a completely different matter – the variable “valid” is being updated under some condition. You can do that with:

valid = valid .or. x > 0

but it is very easy to make a mistake. Code like:

if ( string(1:5) == ’start’ ) then

raises a few questions:

o Is “start” meant to be a prefix? If so, then this code is fine, although the fixed substring index is worrisome – it is easy to miscount, especially with long prefixes. It would be less error-prone to use something along these lines:

if ( index( string, ’start’ ) == 1 ) then

o Is the string meant to contain nothing more than the word “start”? Then

if ( string == ’start’ ) then

is more appropriate. The construction indicates yet another misunderstanding: in Fortran strings are padded with blanks (spaces) before such a comparison. There is no need for taking the substring.

Watch out for uninitialised variables. In Fortran variables do not get a default value. Some compilers can detect particular classes of uninitialised variables, others can generate code where each variable and array is initialised with a special value so that you can detect initialisation problems at run-time (note that the program will probably be slower because of this!) But such options are no substitute for proper initialisation – that is: make the initialisation explicitly visible in the source code.4

If you initialise a variable via a DATA statement or initialisation expression like:

integer :: count = 0

the initialisation is done once, and once only! This is in contrast to C-like languages where the above line is shorthand for:

integer :: count ...

count = 0

There is a rather subtle “gotcha” involving data in modules and COMMON blocks: if no active subroutine or function refers to the module (via use) or the COMMON block, the data they contain are discarded – or at least the Fortran standard says they are. In practice few if any implementations really do that, but it is something to keep in mind.

Beware of micro-optimisations:

do i = 1, n

if ( x(i) .ne. 1.0 ) y(i) = x(i) * y(i) enddo

There is no reason to do something like this:

4

Some compilers can find uninitialised variables if both optimisation and warnings are turned (B. Kleb, 2009). For instance: gfortran -Wall -Wextra -O2 will give quite some information.

(7)

o The program will be slightly slower, unless you have a very smart compiler, as in every iteration the condition has to be checked.

o The time saved by not multiplying by the trivial factor is marginal. o The code is more difficult to read

o You do not gain accuracy

A somewhat surprising feature of Fortran is that lower bounds for array indices are not passed on to subroutines or functions, not even with intrinsic functions. The following program illustrates this:

program chkidx

... real, dimension(-3:3) :: x x = 0.0

x(0) = 1.0

write(*,*) ’Maximum at: ’, maxloc(x) end program chkidx

The printed index is 4, and not 0! If you think about it, it is a quite reasonable feature: if lower bounds were passed on, then every subroutine and function would have to take care of it, loops like:

do i = 1, n

... y(i) = x(i) * y(i) enddo

would have to be consistently written as:

do i = lbound(x),ubound(x) ... y(i) = x(i) * y(i) enddo

As lower bounds other than 1 are fairly rare, it would put a large burden on the programmer.

Avoid Traps

In any programming language there are perfectly legal and well-defined constructs or idioms that nevertheless should be avoided. Fortran is no exception.

Adequate error handling

Opening and reading a file in Fortran is – with respect to errors that may occur – pretty easy: Method 1: you do not explicitly handle errors and then the program will take care of it itself with drastic yet clear measures (it stops and prints an error message, possibly with details of where it occurred)

Method 2: you use the keywords ERR=, END= or IOSTAT=.

If you choose the second method, you have the opportunity to recover from the error. But you also have the opportunity to ignore it:

open( 10, file = ’non-existing-file.inp’, status = ’old’, iostat = ierr )

! Happily go on – ignore ierr

Comparing reals

Common wisdom has it that you should not compare reals for (strict) equality or inequality, because of the finite precision.5 Instead you should use some margin or a “fuzzy” comparison (cf. Knoble, 2009). Actually this rule can be relaxed in one way: if the mathematical real

5

(8)

number can be represented exactly – a value like -999.0 which is sometimes used to indicate missing values, is exactly representable – and :

real, parameter :: missing = -999.0 if ( x == missing ) then

will work as expected.

On the other hand, checking if a variable is greater than some threshold is just as “dangerous” as checking if it is equal to that value. Consider a thermostat for instance. If the temperature is below a threshold, the heating is turned on and if it is above another threshold, it is turned off. A precise check T < Tmin or T > Tmax may lead to subtle differences in the moments the

heating is turned on or off depending on the precise values of the temperature. A different compiler option might cause the moment to shift.

Mixed precision

When dealing with real constants, take care to use the right precision:

real(kind=kind(1.0d0), parameter :: pi = 3.1415926535897932384626433

may look as if you specify the parameter pi with a large number of decimals, but the actual value is default precision, so on most computers only 6 or 7 decimals are retained.

The same caution holds for expressions where real or complex numbers of different precision are used. The result may have the desired precision, but the accuracy is that of the item of the lowest precision.

Surprises with negative numbers

Fortran defines two functions for dealing with the mathematical modulo operation: mod and modulo. They differ in the way negative arguments are treated. Be sure to chose the right one! But negative numbers can cause more surprises. Suppose you use the minimum function to delimit a term – it may not exceed some maximum value:

x = min( x, xmax )

If your purpose is to make sure that the magnitude of the number x does not exceedxmax,

then the above won’t work for negative x. You would have to use:

x = max( min( x, xmax ), -xmax )

or similar expressions.

Automatic arrays

One construction in Fortran that may lead to problems is the automatic array. As long as the arrays are small, they will fit into the memory set apart for such constructions, usually the

stack. If the arrays are too large though, stack overflows occur and your program simply stops

in mid-air, so to speak. It is a compiler-dependent problem, but if you deal with potentially large arrays, consider using allocated arrays instead. Here is an example:

n = 1 do i = 1,8 n = n * 10

call auto_array( n ) enddo

(9)

... contains

subroutine auto_array( n ) integer :: n

integer, dimension(n) :: array array = n

write(*,*) array(1) end subroutine

The array is defined as an automatic array with increasing size and in many implementations it will taken from the stack. Running the program will at some point leas to a stack overflow as the array simply does not fit anymore. An alternative is to explicitly allocate the array:

subroutine allocated_array( n ) integer :: n

integer, dimension(:), allocatable :: array allocate( array(n) )

array = n

write(*,*) array(1) end subroutine

While this array may at some point become too large as well, the program can at least catch that (via the STAT= keyword). When leaving the routine, the array is automatically

deallocated.

Similar issues exist with array sections: The statement

write( 20 ) value(i,:)

uses an array section and this is probably implemented by the compiler as a temporary array. Whether or not this leads to a stack overflow depends on the size of that temporary array, but it is a risk. Here a simple alternative is to use an implied do-loop:

write( 20 ) (value(i,j), j = 1,size(value,2))

Not just numbers

While it is nonsensical to add one meter to one second, for a computer program these will simply be numbers and it will therefore do the computation without any protest. There are several libraries (cf Petty, 2001) that can help with the correct handling of units. If you have not used them during development of the code, then check the various computations manually for this aspect.

The problem occurs not only with physical units: if your program deals with different currencies, for instance, you need to do very similar checks.

Clean Code

The last category consists of signs of a lack of attention to design, to readability and other aspects that are important for the program in the long run.

If your program, however large, is put in a single file on disk, building it (compiling and linking) is almost trivial. But source files of 100,000 lines of code are not easy to work with – printing is a waste of paper, if you only need to look at a few hundred lines. Two related routines may be thousands of lines apart. So, a conscientious organisation of the code in files, directories and subdirectories is just important as getting the program to work.

(10)

Within the code: if you decide to use both upper-case and lower-case letters (e.g. all keywords are capitalised), do so consistently. It does not matter to the compiler, but it does to the human eye.

Then there is the complexity of the code itself to consider:

If a routine has a lot of tasks, is it necessary to do all of them in that one routine? Could you split it up or add an extra level of routines? This will make the structure clearer.

Routines with a lot of tasks tend to require long argument lists and many lines of code. Understanding them and verifying that the correct arguments are passed is tedious and error-prone work.

Common subexpressions that are often repeated have a number of drawbacks: o Unless the compiler recognises them, they need to be computed every time. o More importantly, if you make a mistake or if the subexpression changes for

another reason, you must change it in several places. It is easy to forget one. o The code is more difficult to read – we must recognise ourselves that parts of

the expressions are the same.

Store the result in an extra variable and use that instead, or if the expressions are similar but not identical (differing in one or two variables, say), it may help to use small functions.

Sometimes not just subexpressions are repeated, but whole groups of statements. Here a function or a subroutine is probably a better solution, as it highlights the role of that group of statements and you concentrate the code in one place, making it easier to consistently correct any bugs or implement an improvement/extension.

Here are a few fragments of code that could be made simpler (all these fragments are derived from actual code):

if ( a+b+c > 0.0 ) then x = a + b + c else x = 0.0 endif if ( x > y ) then x = y endif if ( have_file ) then

call read_data( data1, data2, ... ) else

data1 = 0 data2 = 0 ... Endif

The problem with this last fragment was, that the routine read_data was initialing more variables than the else-block. One could either simply always call the routine and first initialise all these variables inside the routine or initialise the variables and then call the routine:

! Alternative 1 !

call read_data( have_file, data1, data2, ... ) ...

!

subroutine read_data( have_file, data1, data2,... ) ...

data1 = 0 data2 = 0 ...

(11)

! Read the data if ( have_file ) then ... endif end subroutine ! ! Alternative 2 ! data1 = 0 data2 = 0 ... if ( have_file ) then

call read_data( data1, data2, ... ) endif

The advantage over the given code is that the initialisation is taking place first, so that its role is more prominently displayed.

Functions (as opposed to subroutines) should not have side-effects, that is, they should return the same value whenever the same arguments are passed and neither the

arguments or anything else is changed. That way they stay close to the mathematical concept and there is less surprise in using them.

Another reason for function without side-effects is that in a multithreaded program, it becomes insanely difficult to keep the program correct.

Beware of functions with no arguments:

lun = newlun()

open( lun, file = ’myinput’ )

There is an extra reason for this example: compilers are free to assume that the

function will always return the same value and so, the function call could be optimised away.

In general, however, a function with side effects poses restrictions on its use:

X = f(a) - f(a) ...

real function f( y ) real :: y

real, save :: count = 0 f = y + count

count = count + 1 end function

might produce -1 but also 1, depending on the order in which the function is actually called.

One useful advice here: make the function pure – that way the compiler will complain if there are any side-effects (B. Kleb, 2009).

Conclusion

Much of what has been written here is merely common sense – it is not a complete catalogue of things to watch out for – but when you develop a program, it is easy to forget about these aspects. The first goal is, after all, to get the stuff to work. That is perfectly understandable. Taking a step back yourself or showing the code to others helps to get the code into better shape.

(12)

Literature

K. Beck and C. Andres (2004)

Extreme Programming Explained: Embrace Change Addison-Wesley Professional

www.amazon.com/extreme-programming-explained-embrace-change/dp/0321278658 S-A. Boukabara and P. Van Delst (2007)

Standards, Guidelines and Recommendations for Writing Fortran 95 Code

projects.osd.noaa.gov/spsrb/standards_docs/fortran95_standard_rev26sep2007.pdf P. Goodliffe (2007)

Code Craft, the practice of writing excellent code No Starch Press, inc, 2007

L. Hatton (1995)

Safer C: Developing software in high integrity and safety-critical systems McGraw-Hill International Seriesin Software Engineering, 1995

B. Kleb (2009)

Personal communication B. Kleb and others (2009)

F95 Coding standard for the FUN3D project

http://fun3d.larc.nasa.gov/chapter-9.html#f95_coding_standard

H. Knoble (2009)

http://www.personal.psu.edu/faculty/h/d/hdk/fortran.html G.W. Petty (2001)

Automated computation and consistency checking of physical dimensions and units in scientific programs.

Software - Practice and Experience, 31, 1067-1076

Referenties

GERELATEERDE DOCUMENTEN

Een interessante vraag voor Nederland is dan welke landen en sectoren betrokken zijn bij het vervaardigen van de producten die in ons land worden geconsumeerd. Deze paragraaf laat

Op deze manier wordt geprobeerd meer inzicht te krijgen in de rol van de controller bij het plegen van kostenmanipulaties binnen verslaggevingsfraude, aangezien de theorie

2 The Swedes are treating gender-conforming children the way we once treated gender-variant children, formerly called 'tomboy girls' and 'sissy boys'.. These gender-variant kids

Political scientists found that Hollywood movies are better able to change attitudes – in a left-wing direction – than advertising or news reports.. The research of Todd Adkins, of

Given that the growth of total assets, market-to-book ratio, research and development expenses, and market value of equity do not act as the use of derivatives, there is no

In other words, a comprehensive and systematic vascular plant phenology taking into account vegetative and reproductive events of both alien and indigenous species representative

In short, we conclude that for all banking products a wide range of factors are related to the reported propensities to switch the current account, savings account and mortgage loan

H 3 Watching an online video ad on a touch-based device (vs. non-touch-based devices) positively affects the customer’s probability to skip