Bentley Optimization Rules for Programming: Part 1
By: Gunavaran Brihadiswaran
Tags: Performance Engineering, Programming, Optimization, Computer Science, Coding

Bentley Optimization Rules for Programming: Part 1

Performance Engineering Series 01

Photo by Hitesh Choudhary on Unsplash

In his book “Writing Efficient Programs” published in 1982, Jon Louis Bentley outlined several rules for optimizing the code. In this article, we will discuss a new set of Bentley Rules adapted from his original work which focus on reducing the work (which is the total number of operations that need to be performed).

We can divide these rules into four categories: rules related to data structures, loops, logic, and functions.

Rules related to Data Structures

1. Packing and Encoding

The idea of packing is to store more than one data value in a machine word. For example, consider the date “18 September 2020”. If we store this date as a string it would require 17 bytes (2 bytes for the day, 9 bytes for the month, 4 bytes for the year, and 2 blank spaces), which is more than two double (64 bit) words.

We can pack this entire date into one double word. Let’s assume that the dates are in the range 4096 BC to 4096 AD. Then we only need 13 bits to represent the year [-4096 to 4096], 4 bits to represent the month (since we have 12 months) and 5 bits to represent the day (since we have 31 days). Hence we can design a struct date_t as shown below.

typedef struct {

int year: 13;

int month: 4;

int day: 5;

} date_t;

Another approach is to encode the date. We have 8192 years in total (4096 years before Christ and 4096 years after Christ). Therefore the total number of possible dates is 365.25 x 8192 (approximately 3 million), which can be represented using 22 bits (2²² is approximately 4 million).

In packing, we can extract the year, month, and day directly but in the second encoding approach we need additional computation to extract those details.

2. Augmentation

The idea of augmentation is to add additional information to the data structure so that we could perform the common/frequent operations easily. For example, consider a scenario where we have a singly linked list and we often need to access the middle and last element. In a typical linked list, we only store the pointer to the first element or the header. But by storing the pointers to the middle and last elements will eliminate the need to traverse the linked list to find the middle and last elements.

Image by author

3. Precomputation

The idea of precomputation is to do the computations beforehand in order to avoid those during mission-critical times. For example, let’s assume that we need to calculate the binomial coefficients which are calculated using the following formula.

These values can be arranged to form Pascal’s triangle as shown below.

For larger values of n and k, this can be time-consuming (which is undesirable in a time-critical instance) as it involves a large number of multiplications. We can precompute this table during the program initialization and then perform a table lookup when necessary. A table lookup is faster than computing the coefficient.

4. Compile-Time Initialization

Instead of computing Pascal’s triangle during the program execution, we can store these values during compilation as shown below since these values are constants. Using this approach, we can avoid computing this table during the program execution. During the execution, it is only a matter of table lookup.

5. Caching

The idea of caching is to store the values that have been computed recently to avoid recomputing those values. For example, look at the following code segment which returns the length of hypotenuse given the other two sides of the triangle.

double hypotenuse (double A, double B){

return sqrt(A*A + B*B);

}

If we often calculate the hypotenuse for the same values of A and B consecutively, we can avoid recomputation using the following code.

double cached_A = 0.0;

double cached_B = 0.0;

double cached_H = 0.0;



double hypotenuse (double A, double B){

if(A == cached_A && B == cached_B){

return cached_H;

}



cached_A = A;

cached_B = B;

cached_H = sqrt(A*A + B*B);

return cached_H;

}

6. Sparsity

Matrix computations are widely used in the field of engineering. One popular application is solving a system of linear equations by converting them into matrix form. Often, such matrices are sparse (where the majority of the values are zeroes).

By storing only the non-zero values we can save memory as well as reduce the number of operations/multiplications that need to be performed (since multiplying by 0 is always 0). Have a look at the following diagram.

First, we create a one-dimensional array to store the number of non-zero elements in each row (the index of the array can be used to represent the row number). Then we can create a dictionary where column numbers are the keys and corresponding cell values are the values. In the above diagram, row-1 contains 4 non-zero values and those values are in columns 1, 2, 4, and 5.

However, the multiplication function needs to be modified accordingly. It is important to note that if the matrix is not sparse (a dense matrix), this approach would negatively affect the performance.