4.2 Pattern Matching and Transform
4.2.2 Pattern Transformation
Our analyser matches the function with append array pattern, and then can perform the code transformation on that function to make use of preallocated array pattern and improve the efficiency of program execution.
Definition 4.11 (From Append Array Pattern to Preallocate Array Pattern) Append array pattern adds n items to the array by using function append in
68
each loop iteration, but introduce expensive overheads of array copying. But the append array pattern can be transformed into preallocate array pattern with estimated array size:
arr size(ARR) = loop iters(V )∗ n = (B − INIT ) × n, OP is < (B − INIT + 1) × n, OP is ≤
(see append array pattern 4.9 and incremental while-loop pattern 4.7). Append array pattern
hARRi = [hX i; 0 ] hV i = hINIT i
while hV i hOPi hBi:
hS0 not updating V, B or ARRi
hV i = hV i + 1
hS1 not updating V, B or ARRi
hARRi = append(hARRi, hitem1i)
...
hSn not updating V, B or ARRi
hARRi = append(hARRi, hitemni)
hSn+1 not updating V, B, ARRi
=⇒ Preallocate array pattern
hARRi = [hX i; arr size(ARR)] hV i = hINIT i
while hV i hOPi hBi:
hS0 not updating V, B or ARRi
hV i = hV i + 1
hS1 not updating V, B or ARRi
hARR[size]i = hitem1i
size = size + 1 ...
hSn not updating V, B or ARRi
hARR[size]i = hitemni
size = size + 1
hSn+1 not updating V, B, ARRi
Array variable is ARR and loop variable is V ; expression X represents the initial value of array item; expression arr size(ARR) denotes the estimated size of array ARR; expression INIT is the initial value of loop variable V ; OP stands for the comparing operator of loop condition; B is the loop bound; S1 ...n+1 each represents a sequence of code which does not assign to/update
loop variable V , loop bound B or array variable ARR; item1 ...n+1 each is the
array item; variable size keeps track of the size of array ARR.
Preallocate array pattern uses the estimate of array size to allocate all the nec- essary space in memory for array VAR before loop executed, so that expensive array copying can be replaced with fast and constant-time array update. The
performance of resulting code therefore can be improved. We will illustrate the pattern transformation with the below example.
1 // Append an array one by one at each iteration 2 function f(byte[] input) -> (byte[] output): 3 int pos = 0
4 output = [0b;0] // Empty output array
5 while pos < |input|:// Iterate each byte in ’input’ array 6 byte index = Int.toUnsignedByte(pos)
7 byte item = input[pos] 8 pos = pos + 1
9 // Append index and item to ’output’ array 10 output = append(output, index)
11 output = append(output, item) 12 return output
Listing 4.7: Append array Whiley program
Example 4.6 Consider the above example in Listing 4.7. Suppose variable input is a byte array. Function f takes it as input and produces an array output . The function starts with an empty array and uses function append to copy the output array and add a new item onto the end of array.
The pattern transformation has two main steps: estimating array size and transforming the code.
Array Size Estimation We firstly find the pattern of function f and then
obtain the array size information to perform pattern transformation. Since function f is matched with incremental while-loop, we can know the number of loop iterations is the length of array input (see Definition4.7):
loop iters(pos) =|input| − 0 = |input|
Function f is further matched with append array pattern. As the loop makes two append function calls every iteration, we can estimate the size of array output (see append array pattern 4.9):
arr size(output) = loop iters(pos)× 2 = |input| × 2
With above information, we can allocate array output with double the size of array input before the loop. Then inside the loop, we gradually update array output with items and count its array size. Finally, outside the loop we then have array output filled up with all the items.
70
Code Transformation According to pattern transformation in Definition 4.11,
we can change function f to the following program:
1 // Function ’f’ uses resize array pattern
2 function f(byte[] input) -> (byte[] output): 3 int pos = 0
4 // Pre−allocate output array with 2x input array size 5 output = [0b;2*|input|]
6 int size = 0// Actual array size
7 while pos < |input|: // Iterate each byte in ’input’ array 8 byte index = Int.toUnsignedByte(pos)
9 byte item = input[pos]
10 output[size] = index// Fill in the array with in−place update 11 size = size + 1
12 output[size] = item 13 size = size + 1 14 pos = pos + 1
15 output = resize(output, size)// Resize output array to actual size 16 return output
Listing 4.8: Tranformed Function f using Resize Array Pattern
Listing 4.8 shows that array output is pre-allocated with the size large enough to hold all its items, so that any out-of-bound array error can be avoided during loop iterations executed. And we use fast array update, instead of slow array append, to populate array output. And at the end of function, we reduce array output to precise-sized one to save the memory space.
Time Complexity One may be interested in the efficiency improvement
obtained from our pattern transformation. Assume the array size is n. The complexity of performing append array pattern is calculated as below.
• Function append has a linear-time complexity O(n).
• Function append is repeatedly invoked within a loop, so the total number of function calls is the same as loop iterations or n.
So in the worse case the array append pattern is quadratic-timed complexity O(n∗ n) = O(n2). However, the preallocate array pattern utilises in-place
array update and thus has linear-time complexity O(n). Therefore, we can conclude preallocate array pattern is more efficient than append array.
Copy Elimination Analysis
Our project (Weng et al., 2017) develops several function analyses, copy elim- ination analysis and de-allocation analysis to extract the properties of each WyIL code, and then assist our code generator to apply code optimisation and produce efficient code.
5.1
Function Analyses
The function analysers all employ a conservative strategy to extract variable information from functions, and store that information in order to support the copy and de-allocation analysers to make safe code optimisation, while improving the efficiency.
Each function analyser traverses all the functions and processes specific information. Our project includes three function analysers:
• The read-write analyser checks if a variable is or may be read and written inside a function
• The return analyser checks if a variable is or may be returned by a function.
• The live analyser checks if a variable is alive or used after the code of a function.
72