SPO600 Project – Stage 2 Summary

Stage two of this project is to implement optimizations and perform tests to determine if there is an improvement in hash function performance. OlegDB uses three different Murmur3 hash functions. The first two hash functions are optimized for x86 platforms and the third hash function is optimized for x64 platforms. Since I am testing hash function performance on an AArch64 system (AArchie) and a x86_64 system (Xerxes), I have decided to try to optimize all three functions. On AArchie, the first hash function runs about 14% faster and the second hash function runs about 17% faster after compiling with -O3 option, which is a significant improvement in performance. The third hash function runs about 0.02% faster after compiling with -O3 option, which is an extremely small improvement in performance. For the third hash function, when we consider only the results with -O2 option, the scenario that changes the code from nblocks*16 to len produces the best performance. When we consider only the results with -O3 option, the scenario with no code changes has the best performance. When we consider all of the results, the third hash function produces the best performance when it is compiled with -O3 option with no code changes. However, the function runs only about 0.02% faster when it is compiled with -O3 option, so this will not have a noticeable improvement in performance for OlegDB. Surprisingly, changing the code from nblocks*16 to len produces the best performance only when it is compiled with -O2 option. When compiling with -O3 option, changing the code from i*2 to i+i or from nblocks*16 to len leads to poorer performance.

On Xerxes, the first hash function runs about 55% faster and the second hash function runs about 60% faster after compiling with -O3 option. Both functions experience a huge improvement in performance after compiling with -O3 option. The third hash function runs slightly slower after compiling with -O3 option. In an attempt to further optimize the first two hash functions, I change the code from nblocks*4 to len for the first function and from nblocks*16 to len for the second function. When compiling with -O3 option, changing the code for both functions further improves performance by an extremely small amount. As a result, compiling with -O3 option combined with the code change to len for both functions produces the best performance and is the most optimized case. It also means that compiling with -O3 option is responsible for nearly all of the improvement in hash function performance.

SPO600 Project – Stage 2

For the second stage of my SPO600 project, I need to implement optimizations to a hash function in the open source software package that I have chosen, which is OlegDB. I need to prove that the optimized hash function will produce the same results as the original hash function. I need to determine the performance of the optimized hash function on an AArch64 system and compare the results with the performance of the original hash function to find out if there is improvement in performance. Similar tests will also be performed on a x86_64 system to determine if we get similar results.

As I have mentioned in my previous posts, OlegDB uses three different Murmur3 hash functions, which are optimized for x86 and x64 platforms. The first function is optimized for a 32-bit machine and it produces a 32-bit output. The second function is also optimized for a 32-bit machine but it produces a 128-bit output. The third function is optimized for a 64-bit machine and it produces a 128-bit output. Each hash function will output a different hash value. For the first stage of this project, I have benchmarked all three hash functions since they are in the same file and are linked with other functions. Although I only need to optimize one hash function for the project, I will actually try to optimize all three hash functions and see what I get since the hash functions are in the same file. Here is the code from my benchmark script benchmark.c for the first stage of this project:

/* Murmur3 benchmarking */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include "murmur3.h"
#define SIZE 100000

/*-----------------------------------------------------------------------------
* Platform-specific functions and macros
*/

#ifdef __GNUC__
#define FORCE_INLINE __attribute__((always_inline)) inline
#else
#define FORCE_INLINE
#endif

static FORCE_INLINE uint32_t rotl32 ( uint32_t x, int8_t r )
{
  return (x << r) | (x >> (32 - r));
}

static FORCE_INLINE uint64_t rotl64 ( uint64_t x, int8_t r )
{
  return (x << r) | (x >> (64 - r));
}

#define            ROTL32(x,y)    rotl32(x,y)
#define ROTL64(x,y)   rotl64(x,y)

#define BIG_CONSTANT(x) (x##LLU)

/*-----------------------------------------------------------------------------
* Block read - if your platform needs to do endian-swapping or can only
* handle aligned reads, do the conversion here
*/

#define getblock(p, i) (p[i])

/*-----------------------------------------------------------------------------
* Finalization mix - force all bits of a hash block to avalanche
*/

static FORCE_INLINE uint32_t fmix32 ( uint32_t h )
{
  h ^= h >> 16;
  h *= 0x85ebca6b;
  h ^= h >> 13;
  h *= 0xc2b2ae35;
  h ^= h >> 16;

  return h;
}

/* ---------- */

static FORCE_INLINE uint64_t fmix64 ( uint64_t k )
{
  k ^= k >> 33;
  k *= BIG_CONSTANT(0xff51afd7ed558ccd);
  k ^= k >> 33;
  k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
  k ^= k >> 33;

  return k;
}

/*-----------------------------------------------------------------------------*/

int main() {
  uint32_t hash[4];
  uint32_t seed = 40;
  int i;
  clock_t startTime1, startTime2, startTime3;
  clock_t endTime1, endTime2, endTime3;
  double totalTime1, totalTime2, totalTime3;

  char *samples = (char*)calloc(SIZE, sizeof(char));
  srand(1);
  // Generate random sample of characters
  for (i = 0; i < SIZE; i++) {
            samples[i] = (random() % 26) + 'A';
  }

  startTime1 = clock();  // Start time
  for (i = 0; i < SIZE; i++) {
  MurmurHash3_x86_32(samples, strlen(samples), seed, hash);
  }
  endTime1 = clock();  // End time
  totalTime1 = (double)(endTime1 - startTime1) / CLOCKS_PER_SEC;          // Total execution time

  printf("x86_32:  %08x\n", hash[0]);
  printf("Total time: %lf seconds\n", totalTime1);

  startTime2 = clock(); // Start time
  for (i = 0; i < SIZE; i++) {
  MurmurHash3_x86_128(samples, strlen(samples), seed, hash);
  }
  endTime2 = clock();  // End time
  totalTime2 = (double)(endTime2 - startTime2) / CLOCKS_PER_SEC;          // Total execution time

  printf("x86_128: %08x %08x %08x %08x\n",
         hash[0], hash[1], hash[2], hash[3]);
  printf("Total time: %lf seconds\n", totalTime2);

  startTime3 = clock(); // Start time
  for (i = 0; i < SIZE; i++) {
  MurmurHash3_x64_128(samples, strlen(samples), seed, hash);
  }
  endTime3 = clock();  // End time
  totalTime3 = (double)(endTime3 - startTime3) / CLOCKS_PER_SEC;          // Total execution time

  printf("x64_128: %08x %08x %08x %08x\n",
         hash[0], hash[1], hash[2], hash[3]);
  printf("Total time: %lf seconds\n", totalTime3);

  return 0;
}

void MurmurHash3_x86_32 ( const void * key, int len,
                          uint32_t seed, void * out )
{
  const uint8_t * data = (const uint8_t*)key;
  const int nblocks = len / 4;
  int i;

  uint32_t h1 = seed;

  uint32_t c1 = 0xcc9e2d51;
  uint32_t c2 = 0x1b873593;

  /*----------
  * body
  */

  const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);

  for(i = -nblocks; i; i++)
  {
    uint32_t k1 = getblock(blocks,i);

    k1 *= c1;
    k1 = ROTL32(k1,15);
    k1 *= c2;

    h1 ^= k1;
    h1 = ROTL32(h1,13);
    h1 = h1*5+0xe6546b64;
  }

  /*----------
  * tail
  */

  const uint8_t * tail = (const uint8_t*)(data + nblocks*4);

  uint32_t k1 = 0;

  switch(len & 3)
  {
  case 3: k1 ^= tail[2] << 16;
  case 2: k1 ^= tail[1] << 8;
  case 1: k1 ^= tail[0];
          k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
  };

  /* finalization */

  h1 ^= len;

  h1 = fmix32(h1);

  *(uint32_t*)out = h1;
}

void MurmurHash3_x86_128 ( const void * key, const int len,
                           uint32_t seed, void * out )
{
  const uint8_t * data = (const uint8_t*)key;
  const int nblocks = len / 16;
  int i;

  uint32_t h1 = seed;
  uint32_t h2 = seed;
  uint32_t h3 = seed;
  uint32_t h4 = seed;

  uint32_t c1 = 0x239b961b;
  uint32_t c2 = 0xab0e9789;
  uint32_t c3 = 0x38b34ae5;
  uint32_t c4 = 0xa1e38b93;

  /* body */

  const uint32_t * blocks = (const uint32_t *)(data + nblocks*16);

  for(i = -nblocks; i; i++)
  {
    uint32_t k1 = getblock(blocks,i*4+0);
    uint32_t k2 = getblock(blocks,i*4+1);
    uint32_t k3 = getblock(blocks,i*4+2);
    uint32_t k4 = getblock(blocks,i*4+3);

    k1 *= c1; k1  = ROTL32(k1,15); k1 *= c2; h1 ^= k1;

    h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b;

    k2 *= c2; k2  = ROTL32(k2,16); k2 *= c3; h2 ^= k2;

    h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747;

    k3 *= c3; k3  = ROTL32(k3,17); k3 *= c4; h3 ^= k3;

    h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35;

    k4 *= c4; k4  = ROTL32(k4,18); k4 *= c1; h4 ^= k4;

    h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17;
  }

  /* tail */

  const uint8_t * tail = (const uint8_t*)(data + nblocks*16);

  uint32_t k1 = 0;
  uint32_t k2 = 0;
  uint32_t k3 = 0;
  uint32_t k4 = 0;

  switch(len & 15)
  {
  case 15: k4 ^= tail[14] << 16;
  case 14: k4 ^= tail[13] << 8;
  case 13: k4 ^= tail[12] << 0;
           k4 *= c4; k4  = ROTL32(k4,18); k4 *= c1; h4 ^= k4;

  case 12: k3 ^= tail[11] << 24;
  case 11: k3 ^= tail[10] << 16;
  case 10: k3 ^= tail[ 9] << 8;
  case  9: k3 ^= tail[ 8] << 0;
           k3 *= c3; k3  = ROTL32(k3,17); k3 *= c4; h3 ^= k3;

  case  8: k2 ^= tail[ 7] << 24;
  case  7: k2 ^= tail[ 6] << 16;
  case  6: k2 ^= tail[ 5] << 8;
  case  5: k2 ^= tail[ 4] << 0;
           k2 *= c2; k2  = ROTL32(k2,16); k2 *= c3; h2 ^= k2;

  case  4: k1 ^= tail[ 3] << 24;
  case  3: k1 ^= tail[ 2] << 16;
  case  2: k1 ^= tail[ 1] << 8;
  case  1: k1 ^= tail[ 0] << 0;
           k1 *= c1; k1  = ROTL32(k1,15); k1 *= c2; h1 ^= k1;
  };

  h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len;

  h1 += h2; h1 += h3; h1 += h4;
  h2 += h1; h3 += h1; h4 += h1;

  h1 = fmix32(h1);
  h2 = fmix32(h2);
  h3 = fmix32(h3);
  h4 = fmix32(h4);

  h1 += h2; h1 += h3; h1 += h4;
  h2 += h1; h3 += h1; h4 += h1;

  ((uint32_t*)out)[0] = h1;
  ((uint32_t*)out)[1] = h2;
  ((uint32_t*)out)[2] = h3;
  ((uint32_t*)out)[3] = h4;
}

void MurmurHash3_x64_128 ( const void * key, const int len,
                           const uint32_t seed, void * out )
{
  const uint8_t * data = (const uint8_t*)key;
  const int nblocks = len / 16;
  int i;

  uint64_t h1 = seed;
  uint64_t h2 = seed;

  uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5);
  uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f);

  const uint64_t * blocks = (const uint64_t *)(data);

  for(i = 0; i < nblocks; i++)
  {
    uint64_t k1 = getblock(blocks,i*2+0);
    uint64_t k2 = getblock(blocks,i*2+1);

    k1 *= c1; k1  = ROTL64(k1,31); k1 *= c2; h1 ^= k1;

    h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;

    k2 *= c2; k2  = ROTL64(k2,33); k2 *= c1; h2 ^= k2;

    h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
  }

  const uint8_t * tail = (const uint8_t*)(data + nblocks*16);

  uint64_t k1 = 0;
  uint64_t k2 = 0;

  switch(len & 15)
  {
  case 15: k2 ^= (uint64_t)(tail[14]) << 48;
  case 14: k2 ^= (uint64_t)(tail[13]) << 40;
  case 13: k2 ^= (uint64_t)(tail[12]) << 32;
  case 12: k2 ^= (uint64_t)(tail[11]) << 24;
  case 11: k2 ^= (uint64_t)(tail[10]) << 16;
  case 10: k2 ^= (uint64_t)(tail[ 9]) << 8;
  case  9: k2 ^= (uint64_t)(tail[ 8]) << 0;
           k2 *= c2; k2  = ROTL64(k2,33); k2 *= c1; h2 ^= k2;

  case  8: k1 ^= (uint64_t)(tail[ 7]) << 56;
  case  7: k1 ^= (uint64_t)(tail[ 6]) << 48;
  case  6: k1 ^= (uint64_t)(tail[ 5]) << 40;
  case  5: k1 ^= (uint64_t)(tail[ 4]) << 32;
  case  4: k1 ^= (uint64_t)(tail[ 3]) << 24;
  case  3: k1 ^= (uint64_t)(tail[ 2]) << 16;
  case  2: k1 ^= (uint64_t)(tail[ 1]) << 8;
  case  1: k1 ^= (uint64_t)(tail[ 0]) << 0;
           k1 *= c1; k1  = ROTL64(k1,31); k1 *= c2; h1 ^= k1;
  };

  h1 ^= len; h2 ^= len;

  h1 += h2;
  h2 += h1;

  h1 = fmix64(h1);
  h2 = fmix64(h2);

  h1 += h2;
  h2 += h1;

  ((uint64_t*)out)[0] = h1;
  ((uint64_t*)out)[1] = h2;

}

For stage two of this project, I have decided to make two small changes to this code. The benchmark results from stage one show that it takes about 5 to 7 seconds to execute the hash function 100000 times. Now, I will increase the sample data size from 100000 to 200000 in order to execute the hash function 200000 times in order to produce more accurate and stable results. The second change is to run only one hash function when I run my benchmark program. For stage one, I execute all three hash functions when I run my benchmark program. Now, I will execute one function at a time by commenting out the code for the other two functions so that they will not get executed. This becomes easier for me to compare the results for each function before and after optimization.

Optimize hash function – first attempt

There are four major approaches that I can use to optimize the hash function. I will start off with the easiest approach, which is to alter the current build options for OlegDB. The Makefile for OlegDB contains the compilation command “gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2”, which shows that -O2 option is the optimization option used to compile and build OlegDB. I will compile my benchmark program with the -O3 option instead of the -O2 option to test if there is an improvement in hash function performance. The -O3 option turns on all optimization flags specified by the -O2 option as well as other optimization flags including the ones that enable vectorization.

Before I run my benchmark program, I check who is logged in to AArchie (AArch64 architecture) by using the “who” command. I am the only user logged in, so that is good. Now, I compile my benchmark program using -O2 option using the same compilation command as in the Makefile: “gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark”. I will run this program at least ten times for each function and manually calculate the average time to produce more accurate results. I will remove any results that are much higher or much lower than the rest of the results. I will perform the same steps for each function using -O3 option. Here are all of the results:

First hash function (MurmurHash3_x86_32):
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.837361 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.843444 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.842607 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.832756 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.846980 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.835534 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.846635 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.837680 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.844609 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 28.846933 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268407 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268424 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268392 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268794 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268390 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268788 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268782 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268397 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268774 seconds
[cslam4@aarchie include]$ ./benchmark
x86_32:  10a60322
Total time: 25.268388 seconds

Second hash function (MurmurHash3_x86_128):
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.090318 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.077951 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.090293 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.082010 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.090249 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.081085 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.081272 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.082428 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.090463 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.080640 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505954 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.506262 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505999 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505963 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505981 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.506277 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505947 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505934 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505944 seconds
[cslam4@aarchie include]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 21.505959 seconds

Third hash function (MurmurHash3_x64_128):
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.246425 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247482 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247506 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.246145 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247472 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245866 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245888 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245845 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245829 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247447 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242796 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241696 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241691 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241734 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242737 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241841 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241739 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241684 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241708 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242766 seconds

The benchmark results above show that for each function, the hash value produced by compiling with -O2 option is the same as the -O3 option. The average execution time for the first hash function is 28.841451 seconds with -O2 option and 25.268550 seconds with -O3 option. Execution time for -O3 option is about 14.1397% faster than -O2 option. The average execution time for the second hash function is 25.084668 seconds with -O2 option and 21.506018 seconds with -O3 option. Execution time for -O3 option is about 16.6402% faster than -O2 option. The average execution time for the third hash function is 21.246587 seconds with -O2 option and 21.242036 seconds with -O3 option. Execution time for -O3 option is about 0.0214% faster than -O2 option, which is insignificant. The first two hash functions, which have been optimized for x86 platforms, have significantly improved in performance after compiling with -O3 option. The third hash function, which has been optimized for x64 platforms, has an extremely small improvement in performance after compiling with -O3 option. Although the difference in execution time is very small, the individual results are very consistent, so it is safe to conclude that there is a very small improvement in performance after compiling with -O3 option.

Optimize hash function – second attempt

After changing the build options, my next step is to change the hash function code and/or improve on existing algorithms to allow the compiler to better optimize the function. For this part, I will only try to optimize the third hash function since it has been optimized for x64 platforms. Within the hash function, there are two lines of code that call the getblock function and the second argument that is provided to the function is i*2+0 or i*2+1. I will replace the multiplication operation (expensive operation) with an addition operation (cheaper operation) to test if there is an improvement in performance. Here are the actual code changes:

Before:
uint64_t k1 = getblock(blocks,i*2+0);
uint64_t k2 = getblock(blocks,i*2+1);

After:
uint64_t k1 = getblock(blocks,i+i+0);
uint64_t k2 = getblock(blocks,i+i+1);

Within the hash function, there is a line of code that performs the operation nblocks*16. At the beginning of the function, nblocks = len / 16 and the values of nblocks and len do not change throughout the function. nblocks and len are both constants and are both integers. Therefore, nblocks*16 = len and I can eliminate one operation by replacing the multiplication operation with the constant integer len to test if there is an improvement in performance. Here are the actual code changes:

Before:
const uint8_t * tail = (const uint8_t*)(data + nblocks*16);

After:
const uint8_t * tail = (const uint8_t*)(data + len);

I will test the performance changes from these two changes independently by comparing the performance before and after each change. I will test this change with -O2 option and with -O3 option. I will use the previous results above for the third function as the performance before any code changes. For the last test, I aggregate the two changes and see if I get more improvement in performance. Here are all of the results:

Code change: i*2 to i+i
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245887 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245893 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247485 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247428 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247471 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247405 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247831 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245855 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247506 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.247427 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241696 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241753 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243036 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241705 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241673 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242911 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.241797 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242779 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243747 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242422 seconds

Code change: nblocks*16 to len
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244150 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243243 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243346 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243282 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243260 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243597 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243112 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243495 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244536 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243777 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244690 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243704 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244652 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244186 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244725 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244738 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243751 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244730 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244395 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244749 seconds

Code changes: i+i and len
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243219 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244473 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243308 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243764 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244765 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.242662 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243585 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243692 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243600 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244205 seconds
[cslam4@aarchie include]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244691 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244195 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244657 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244375 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244669 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.245229 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244615 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.243887 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244550 seconds
[cslam4@aarchie include]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 21.244866 seconds

For easier analysis and comparison, here are the results in table format showing the average execution time in seconds for each scenario:

 

-O2 option

-O3 option

No code changes

21.246587

21.242036

Code change: i+i

21.247016

21.242349

Code change: len

21.243577

21.244429

Code changes: i+i and len

21.243725

21.244570

The benchmark results above show that the hash value that is produced by the function does not change after making the two code changes. The results shown in the table above show that the execution time for -O3 option is faster than -O2 option after changing the code from i*2 to i+i. Surprisingly, the execution time for -O3 option is slightly slower than -O2 option after changing the code from nblocks*16 to len or after making both changes to the code (i+i and len). It is also surprising to see that after changing the code from i*2 to i+i, execution time increases from 21.246587 seconds to 21.247016 seconds with -O2 option and increases from 21.242036 seconds to 21.242349 seconds with -O3 option. After changing the code from nblocks*16 to len, execution time decreases from 21.246587 seconds to 21.243577 seconds with -O2 option but increases from 21.242036 seconds to 21.244429 seconds with -O3 option. After making both changes to the code (i+i and len), execution time decreases from 21.246587 seconds to 21.243725 seconds with -O2 option but increases from 21.242036 seconds to 21.244570 seconds with -O3 option. With -O2 option, the scenario that changes the code from nblocks*16 to len produces the fastest execution time. With -O3 option, the scenario with no code changes produces the fastest execution time. When we consider all scenarios, the scenario with no code changes with -O3 option produces the fastest execution time and is the most optimized scenario.

Besides the two small code changes that I have made, it does not seem like there are other code changes or algorithm improvements that can result in further optimization. The code is already optimized for x64 platforms. It uses unsigned 64-bit integer data type (uint64_t), which cannot be changed. This is a very short and simple function that uses simple operations that cannot be changed and does not contain extra or redundant code that can be removed. The “for” loop cannot be broken down into simpler loops that can enable further optimization such as vectorization. The last approach in optimization is to use inline assembly language, which I will not try to attempt because it will be too difficult and I do not think that there is much room for improvement in performance.

Optimize hash functions on a x86_64 system

After completing all of these tests on an AArch64 system, I perform the same tests on a x86_64 system. Same as before, I start off by compiling my benchmark program with the -O2 option and  -O3 option and compare the results to find out if there is an improvement in hash function performance. Here are all of the results:

First hash function (MurmurHash3_x86_32):
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.326450 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.325260 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.328529 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.326400 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.326497 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.325970 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.327895 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.325542 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.326245 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.327000 seconds
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.253111 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.253899 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.252819 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.251787 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.253398 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.249891 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.251963 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.252295 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.251394 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.251593 seconds

Second hash function (MurmurHash3_x86_128):
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.109220 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.106154 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105291 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.108970 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.109499 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.108291 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105891 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.109633 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.107561 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.108617 seconds
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680093 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.682556 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681794 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681999 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.682527 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681467 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681377 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.682325 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.683196 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.682892 seconds

Third hash function (MurmurHash3_x64_128):
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.590477 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.583230 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.590689 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.596024 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.593182 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.589977 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.592558 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.590842 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.590933 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.586058 seconds
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.659552 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.659442 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.660754 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.658774 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.659238 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.658440 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.658631 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.660263 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.660366 seconds
[cslam4@xerxes project]$ ./benchmark
x64_128: 77b15668 9b50cc2f f191a362 5e2f40e3
Total time: 20.658688 seconds

The benchmark results above show that for each function, the hash value produced by compiling with -O2 option is the same as the -O3 option and is also the same as in AArch64 system. The average execution time for the first hash function is 28.326576 seconds with -O2 option and 18.252213 seconds with -O3 option. Execution time for -O3 option is about 55.1953% faster than -O2 option. The average execution time for the second hash function is 25.107910 seconds with -O2 option and 15.682021 seconds with -O3 option. Execution time for -O3 option is about 60.1063% faster than -O2 option. The average execution time for the third hash function is 20.590394 seconds with -O2 option and 20.659413 seconds with -O3 option. Execution time for -O2 option is about 0.3352% faster than -O3 option. The first two hash functions, which have been optimized for x86 platforms, have significantly improved in performance after compiling with -O3 option. The amount of improvement is much greater than the results from AArch64 system. The third hash function, which has been optimized for x64 platforms, has a slightly poorer performance after compiling with -O3 option.

For the next step in optimization, I will only try to optimize the first two hash functions since they have been optimized for x86 platforms. Similar to the code from the third function, for the first function, nblocks*4 = len and I can remove one operation by replacing the multiplication operation with the constant integer len to test if there is an improvement in performance. Here are the actual code changes for the first function:

Before:
const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);
const uint8_t * tail = (const uint8_t*)(data + nblocks*4);

After:
const uint32_t * blocks = (const uint32_t *)(data + len);
const uint8_t * tail = (const uint8_t*)(data + len);

Similar for the second function, nblocks*16 = len and I can remove one operation by replacing the multiplication operation with the constant integer len to test if there is an improvement in performance. Here are the actual code changes for the second function:

Before:
const uint32_t * blocks = (const uint32_t *)(data + nblocks*16);
const uint8_t * tail = (const uint8_t*)(data + nblocks*16);

After:
const uint32_t * blocks = (const uint32_t *)(data + len);
const uint8_t * tail = (const uint8_t*)(data + len);

Here are all of the results:

First function code change: nblocks*4 to len
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.338146 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.335034 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.339766 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.334517 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.336886 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.336308 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.336579 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.339671 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.334557 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 28.339693 seconds
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.249380 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250478 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250085 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250198 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.249767 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250770 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.251328 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.248198 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250863 seconds
[cslam4@xerxes project]$ ./benchmark
x86_32:  10a60322
Total time: 18.250571 seconds

Second function code change: nblocks*16 to len
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.104996 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105777 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105962 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105587 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.104835 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.104848 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.106052 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105744 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.104093 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 25.105452 seconds
[cslam4@xerxes project]$ gcc -Wall -Werror -g3 -O3 -Wstrict-aliasing=2 benchmark.c -o benchmark
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681199 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680173 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680730 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681321 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680365 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680164 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.681815 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680421 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680890 seconds
[cslam4@xerxes project]$ ./benchmark
x86_128: 5672c6e4 47f36990 c6f1d0a0 1234f049
Total time: 15.680898 seconds

Here are the results in table format showing the average execution time in seconds for each scenario:

First function

 

-O2 option

-O3 option

No code changes

28.326576

18.252213

Code change: len

28.337112

18.250161

Second function

 

-O2 option

-O3 option

No code changes

25.107910

15.682021

Code change: len

25.105332

15.680796

The benchmark results above show that the hash value that is produced by the function does not change after making the code change. The results shown in the tables above show that the execution time for -O3 option is faster for both functions after changing the code from nblocks*4 to len for the first function and from nblocks*16 to len for the second function. The execution time for -O2 option is slower after changing the code from nblocks*4 to len for the first function but is faster after changing the code from nblocks*16 to len for the second function. For both functions, the fastest execution time is produced by compiling with -O3 option combined with the respective code change.

Conclusion

All of the tests are first completed on an AArch64 system. My first step in optimization is to compile my benchmark program with -O3 option and test if there is an improvement in hash function performance. The first two hash functions, which have been optimized for x86 platforms, run about 14-17% faster after compiling with -O3 option, which is a significant improvement in performance. The third hash function, which has been optimized for x64 platforms, run about 0.02% faster after compiling with -O3 option, which is a very small improvement in performance. Although the change in performance is extremely small and insignificant, the individual test results are actually very consistent and suggest that there is indeed a very small improvement in performance after compiling with -O3 option. My second step in optimization is to change a section of the code within the third hash function in an attempt to improve hash function performance. I only work with the third hash function because it has been optimized for x64 platforms. The first change is to change the code from i*2 to i+i. The second change is to change the code from nblocks*16 to len. When we consider only the results with -O2 option, the scenario that changes the code from nblocks*16 to len has the best performance. When we consider only the results with -O3 option, the scenario with no code changes has the best performance. When we consider all of the results, compiling with -O3 option with no code changes produces the best performance and is the most optimized case. However, the function runs only about 0.02% faster, which is an extremely small difference, so this will not really improve the performance of OlegDB. The second code change produces the best performance only when compiling with -O2 option. Surprisingly, both code changes result in poorer performance than with no code change when compiling with -O3 option.

After completing all of these tests on an AArch64 system, I perform the same tests on a x86_64 system. The results indicate that the first hash function runs about 55% faster after compiling with -O3 option while the second hash function runs about 60% faster. Both functions have a huge improvement in performance. The third hash function runs slightly slower after compiling with -O3 option. For the second step in optimization, I only work with the first two hash functions because they have been optimized for x86 platforms. I change the code from nblocks*4 to len for the first function and from nblocks*16 to len for the second function. When we consider all of the results, compiling with -O3 option combined with the code change to len produces the best performance and is the most optimized case for both functions. It should be noted that the code change further improves performance by an extremely small amount. Compiling with -O3 option is responsible for nearly all of the improvement in hash function performance.

SPO600 Lab 7 – Inline Assembler

Determine performance of program with assembly language code

With inline assembler, we can insert assembly language code into our source file in order to further optimize our program. I will compare the performance of my volume sample scaling program in lab 6 with the program that includes assembly language code. I download the volume scaling program provided by my instructor that includes assembly language code. In order to produce comparable results, I modify the program by setting the number of samples in the header file vol.h to 500000000 and adding code to calculate and display the amount of processing time required to scale the samples. Here is the modified source code:

// vol_simd.c :: volume scaling in C using AArch64 SIMD
// Chris Tyler 2017.11.29

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "vol.h"
#include <time.h>

int main() {

        int16_t*                in;             // input array
        int16_t*                limit;          // end of input array
        int16_t*                out;            // output array
        clock_t startTime, endTime;
        double totalTime;

        // these variables will be used in our assembler code, so we're going
        // to hand-allocate which register they are placed in
        // Q: what is an alternate approach?
        // A: The alternate approach is not to indicate specific registers that will be used in our assembler code so that we leave it for the compiler to decide what registers to use.
        register int16_t*       in_cursor       asm("r20");     // input cursor
        register int16_t*       out_cursor      asm("r21");     // output cursor
        register int16_t        vol_int         asm("r22");     // volume as int16_t

        int                     x;              // array interator
        int                     ttl;            // array total

        in=(int16_t*) calloc(SAMPLES, sizeof(int16_t));
        out=(int16_t*) calloc(SAMPLES, sizeof(int16_t));

        srand(1);
        printf("Generating sample data.\n");
        for (x = 0; x < SAMPLES; x++) {
                in[x] = (rand()%65536)-32768;
        }

// --------------------------------------------------------------------

        in_cursor = in;
        out_cursor = out;
        limit = in + SAMPLES ;

        // set vol_int to fixed-point representation of 0.5
        // Q: should we use 32767 or 32768 in next line? why?
        // A: We should use 32767 because the maximum value for int16_t data type is 32767 since it has a range between -32768 and 32767.
        vol_int = (int16_t) (0.5 * 32767.0);

        printf("Scaling samples.\n");

        startTime = clock();    // start time
        // Q: what does it mean to "duplicate" values here?
        // A: Duplicating value here means that the value of vol_int (volume factor) is copied to all 8 lanes in vector register 1 so that this value will later be multiplied by the volume sample stored in vector register 0, which rely on SIMD and vectorization.
        __asm__ ("dup v1.8h,w22"); // duplicate vol_int into v1.8h
        while ( in_cursor < limit ) {
                __asm__ (
                        "ldr q0, [x20],#16              \n\t"
                        // load eight samples into q0 (v0.8h)
                        // from in_cursor, and post-increment
                        // in_cursor by 16 bytes

                        "sqdmulh v0.8h, v0.8h, v1.8h    \n\t"
                        // multiply each lane in v0 by v1*2
                        // saturate results
                        // store upper 16 bits of results into v0

                        "str q0, [x21],#16              \n\t"
                        // store eight samples to out_cursor
                        // post-increment out_cursor by 16 bytes

                        // Q: what happens if we remove the following
                        // lines? Why?
                        // A: If we remove these lines which specify input and output operands, we get a segmentation fault. The reason is because we need to specify the input and output operands so that the output value will be stored in in_cursor and the input values will be placed in the registers so that the program knows what values to use.
                        : "=r"(in_cursor)
                        : "r"(limit),"r"(in_cursor),"r"(out_cursor)
                        );
        }
        endTime = clock();      // end time

// --------------------------------------------------------------------

        printf("Summing samples.\n");
        for (x = 0; x < SAMPLES; x++) {
                ttl=(ttl+out[x])%1000;
        }

        // Total CPU time
        totalTime = (double)(endTime - startTime) / CLOCKS_PER_SEC;

        // Display CPU time
        printf("CPU time used to scale samples: %lf seconds\n", totalTime);

        // Q: are the results usable? are they correct?
        // A: The results are correct and usable.
        printf("Result: %d\n", ttl);

        return 0;

}

The answers to the questions in the source code are provided below each question. I compile this program using gcc without -O3 option. I run the lab 6 program version with the best performance without -O3 option, which converts the volume factor to a fix-point integer by multiplying it by a binary number representing a fixed-point value “1”, multiplies this result by each sample value and shifts the result to the right by the correct number of bits to get the scaled sample value. It takes 4.0497 seconds to calculate the scaled sample values and store them in another array. I run the program above, which uses inline assembler and it takes 0.8044 seconds. Now, I compile the program above using gcc with -O3 option to enable a lot of optimization. I run the lab 6 program version with -O3 option and it takes 0.803667 seconds. I run the program above and it takes 0.783166 seconds. As you can see, the processing time for the program above with or without -O3 option is about the same. This makes sense since assembly language code is added to the source code, so the processing time should not change when you change the optimization flag. When comparing the performance with -O3 option, the inline assembler version takes about 2.5% less time than the other method to calculate the scaled sample values and store them in another array. This means that there is a slight improvement in performance when you implement inline assembler.

Investigate assembly language code in SooperLooper

Now, I will be looking for assembly language code in SooperLooper and performing a bit of analysis. SooperLooper is a software that can create audio loops in real-time and it has a few functions such as immediate loop recording, overdubbing, and multiplying. SooperLooper is supported on Linux and Mac OS X platforms and is not supported on Windows platform. If you use Windows, you will need to use Mobius, which is a similar software that is available for Windows.

From the source code for SooperLooper, I am able to find assembly language code in the file atomic.h. This is a header file and most of this file contains assembly language code. Based on the comments in the file, this file contains code that is used to perform atomic operations, which is useful for resource counting. This includes adding, subtracting, incrementing and decrementing a counter. Since inline assembler is architecture-specific, we need to write a unique set of code for each architecture. In this file, I see that there is a unique set of code for each architecture. This file contains code written for the following architectures: m68k, MIPS, s390, Alpha, IA-64, SPARC, i386/x86_64, and PowerPC. There is also a set of code that is used for all other architectures where there is no implementation of strict atomic operations for your hardware. For example, this function deals with the increment operation and is written for PowerPC architecture and it contains assembly language code:

static __inline__ void atomic_inc(atomic_t *v)
{
            int t;

            __asm__ __volatile__(
"1:        lwarx   %0,0,%2\n\
            addic   %0,%0,1\n\
            stwcx.  %0,0,%2\n\
            bne-     1b"
            : "=&r" (t), "=m" (v->counter)
            : "r" (&v->counter), "m" (v->counter)
            : "cc");
}

 

 

 

 

 

Notice that it uses the word volatile so that the code cannot be moved around by the compiler.

The main advantage of using inline assembler in this case is to improve program performance and speed up certain operations by using atomic operations. The drawback is inline assembler is architecture-specific, so we need to create a unique set of code for each architecture in order to produce portable code that can be used across architectures. This has been done with this software and it results in more complex code. I think it is worth the time to incorporate assembly language code as long as it considerably improves program performance and it can be used with most architectures. It just takes a lot of time and it is more costly to write portable code so that it can be used with most architectures.

SPO600 Project – Stage 1 Summary

The first step of stage one of this project is to find an open source software that implements a hash function. It is very difficult to find a software that uses a hash function because it usually does not tell you that it uses a hash function, so you need to figure that out by looking into the source code. Once I find a software with a hash function, my next step is to figure out a way to benchmark the performance of the hash function. It took me a while to create the script to call the three hash functions for benchmarking because I need to deal with a lot of variables. My software OlegDB has optimized the hash functions for x86 and x64 systems, so there are three hash function versions that I need to deal with. I decide to benchmark all three of them so that I can use them in stage two of this project. The hash functions are already optimized, so there is a lower chance that I will find an optimization opportunity. Therefore, working with three hash functions will increase my chance of successfully optimizing a hash function. I look forward to stage two – the implementation stage!

SPO600 Project – Stage 1

Benchmark  the performance of the hash function

As mentioned in my previous post, my next step is to benchmark the performance of the Murmur3 hash function in OlegDB. On AArchie (AArch64 system), after I downloaded the OlegDB tarball and extracted it using the command “tar -xvzf https://github.com/infoforcefeed/OlegDB/archive/v.0.1.5.tar.gz&#8221;, I found the Murmur3 hash function in the file called murmur3.c. Basically, the entire file is specific to the hash function implementation. In fact, the file contains three Murmur3 hash functions, which are optimized for x86 and x64 platforms. The first function is optimized for a 32-bit machine and it produces a 32-bit output. The second function is also optimized for a 32-bit machine but it produces a 128-bit output. The GitHub website for Murmur3 hash states that the second function takes about 86% more time to run than the first function.  The third function is optimized for a 64-bit machine and it produces a 128-bit output.

The hash functions accept input from arguments and are not called from within the murmur3.c script, so I cannot simply compile and run the murmur3.c script and set a timer for each function to determine the time that it takes for each function to execute. Therefore, I need to benchmark the performance of the hash functions by creating and running a script that will call the hash functions by supplying them with the necessary arguments and then setting a timer to calculate the time that it takes for each function to execute. Before I do this, I look at the Makefile file, which is used to compile and build the software. The contents show that the entire OlegDB software is compiled using gcc and using the -02 compiler option and some warning options. In the same directory, I come across the file CONTRIBUTORS and it shows a contributor with a GitHub website with information about Murmur3 implementation. I access the website and I find a file called example.c, which is a sample program. When a user runs the program, the user needs to provide a string as the first argument, which will later be converted into a hash. The program will use that string along with other defined values as arguments to call the three hash functions to compute the hash value for that string and display it on the screen. Each hash function will output a different hash value.

It will make my life easier to use this sample program for benchmarking. I create a script called benchmark.c in the same directory as the header file murmur3.h. I copy all the code in example.c and murmur3.c into benchmark.c. There are only a few changes that I need to make in benchmark.c in order to use it for benchmarking. Since example.c executes each hash function only once, I need to make a change to have the functions execute many times in order to evaluate the performance accurately. I generate random characters to be used as sample data that will be converted to hashes. I predefine the total number of function executions and then I add a “for” loop to each function to have each function execute the predefined number of times. Lastly, I add a “clock” function to set the timer right before and after each hash function to determine the total amount of time that is used to execute each function. Here is the code from benchmark.c:

/* Murmur3 benchmarking */




#include <stdio.h>

#include <stdlib.h>

#include <stdint.h>

#include <string.h>

#include <time.h>

#include "murmur3.h"

#define SIZE 100000




/*-----------------------------------------------------------------------------

* Platform-specific functions and macros

*/




#ifdef __GNUC__

#define FORCE_INLINE __attribute__((always_inline)) inline

#else

#define FORCE_INLINE

#endif




static FORCE_INLINE uint32_t rotl32 ( uint32_t x, int8_t r )

{

  return (x << r) | (x >> (32 - r));

}




static FORCE_INLINE uint64_t rotl64 ( uint64_t x, int8_t r )

{

  return (x << r) | (x >> (64 - r));

}




#define            ROTL32(x,y)    rotl32(x,y)

#define ROTL64(x,y)   rotl64(x,y)




#define BIG_CONSTANT(x) (x##LLU)




/*-----------------------------------------------------------------------------

* Block read - if your platform needs to do endian-swapping or can only

* handle aligned reads, do the conversion here

*/




#define getblock(p, i) (p[i])




/*-----------------------------------------------------------------------------

* Finalization mix - force all bits of a hash block to avalanche

*/




static FORCE_INLINE uint32_t fmix32 ( uint32_t h )

{

  h ^= h >> 16;

  h *= 0x85ebca6b;

  h ^= h >> 13;

  h *= 0xc2b2ae35;

  h ^= h >> 16;




  return h;

}




/* ---------- */




static FORCE_INLINE uint64_t fmix64 ( uint64_t k )

{

  k ^= k >> 33;

  k *= BIG_CONSTANT(0xff51afd7ed558ccd);

  k ^= k >> 33;

  k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);

  k ^= k >> 33;




  return k;

}




/*-----------------------------------------------------------------------------*/




int main() {

  uint32_t hash[4];

  uint32_t seed = 40;

  int i;

  clock_t startTime1, startTime2, startTime3;

  clock_t endTime1, endTime2, endTime3;

  double totalTime1, totalTime2, totalTime3;




  char *samples = (char*)calloc(SIZE, sizeof(char));

  srand(1);

  // Generate random sample of characters

  for (i = 0; i < SIZE; i++) {

            samples[i] = (random() % 26) + 'A';

  }




  startTime1 = clock();  // Start time

  for (i = 0; i < SIZE; i++) {

  MurmurHash3_x86_32(samples, strlen(samples), seed, hash);

  }

  endTime1 = clock();  // End time

  totalTime1 = (double)(endTime1 - startTime1) / CLOCKS_PER_SEC;          // Total execution time




  printf("x86_32:  %08x\n", hash[0]);

  printf("Total time: %lf seconds\n", totalTime1);




  startTime2 = clock(); // Start time

  for (i = 0; i < SIZE; i++) {

  MurmurHash3_x86_128(samples, strlen(samples), seed, hash);

  }

  endTime2 = clock();  // End time

  totalTime2 = (double)(endTime2 - startTime2) / CLOCKS_PER_SEC;          // Total execution time




  printf("x86_128: %08x %08x %08x %08x\n",

         hash[0], hash[1], hash[2], hash[3]);

  printf("Total time: %lf seconds\n", totalTime2);




  startTime3 = clock(); // Start time

  for (i = 0; i < SIZE; i++) {

  MurmurHash3_x64_128(samples, strlen(samples), seed, hash);

  }

  endTime3 = clock();  // End time

  totalTime3 = (double)(endTime3 - startTime3) / CLOCKS_PER_SEC;          // Total execution time




  printf("x64_128: %08x %08x %08x %08x\n",

         hash[0], hash[1], hash[2], hash[3]);

  printf("Total time: %lf seconds\n", totalTime3);




  return 0;

}




void MurmurHash3_x86_32 ( const void * key, int len,

                          uint32_t seed, void * out )

{

  const uint8_t * data = (const uint8_t*)key;

  const int nblocks = len / 4;

  int i;




  uint32_t h1 = seed;




  uint32_t c1 = 0xcc9e2d51;

  uint32_t c2 = 0x1b873593;




  /*----------

  * body

  */




  const uint32_t * blocks = (const uint32_t *)(data + nblocks*4);




  for(i = -nblocks; i; i++)

  {

    uint32_t k1 = getblock(blocks,i);




    k1 *= c1;

    k1 = ROTL32(k1,15);

    k1 *= c2;




    h1 ^= k1;

    h1 = ROTL32(h1,13);

    h1 = h1*5+0xe6546b64;

  }




  /*----------

  * tail

  */




  const uint8_t * tail = (const uint8_t*)(data + nblocks*4);




  uint32_t k1 = 0;




  switch(len & 3)

  {

  case 3: k1 ^= tail[2] << 16;

  case 2: k1 ^= tail[1] << 8;

  case 1: k1 ^= tail[0];

          k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1;

  };




  /* finalization */




  h1 ^= len;




  h1 = fmix32(h1);




  *(uint32_t*)out = h1;

}




void MurmurHash3_x86_128 ( const void * key, const int len,

                           uint32_t seed, void * out )

{

  const uint8_t * data = (const uint8_t*)key;

  const int nblocks = len / 16;

  int i;




  uint32_t h1 = seed;

  uint32_t h2 = seed;

  uint32_t h3 = seed;

  uint32_t h4 = seed;




  uint32_t c1 = 0x239b961b;

  uint32_t c2 = 0xab0e9789;

  uint32_t c3 = 0x38b34ae5;

  uint32_t c4 = 0xa1e38b93;




  /* body */




  const uint32_t * blocks = (const uint32_t *)(data + nblocks*16);




  for(i = -nblocks; i; i++)

  {

    uint32_t k1 = getblock(blocks,i*4+0);

    uint32_t k2 = getblock(blocks,i*4+1);

    uint32_t k3 = getblock(blocks,i*4+2);

    uint32_t k4 = getblock(blocks,i*4+3);




    k1 *= c1; k1  = ROTL32(k1,15); k1 *= c2; h1 ^= k1;




    h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b;




    k2 *= c2; k2  = ROTL32(k2,16); k2 *= c3; h2 ^= k2;




    h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747;




    k3 *= c3; k3  = ROTL32(k3,17); k3 *= c4; h3 ^= k3;




    h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35;




    k4 *= c4; k4  = ROTL32(k4,18); k4 *= c1; h4 ^= k4;




    h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17;

  }




  /* tail */




  const uint8_t * tail = (const uint8_t*)(data + nblocks*16);




  uint32_t k1 = 0;

  uint32_t k2 = 0;

  uint32_t k3 = 0;

  uint32_t k4 = 0;




  switch(len & 15)

  {

  case 15: k4 ^= tail[14] << 16;

  case 14: k4 ^= tail[13] << 8;

  case 13: k4 ^= tail[12] << 0;

           k4 *= c4; k4  = ROTL32(k4,18); k4 *= c1; h4 ^= k4;




  case 12: k3 ^= tail[11] << 24;

  case 11: k3 ^= tail[10] << 16;

  case 10: k3 ^= tail[ 9] << 8;

  case  9: k3 ^= tail[ 8] << 0;

           k3 *= c3; k3  = ROTL32(k3,17); k3 *= c4; h3 ^= k3;




  case  8: k2 ^= tail[ 7] << 24;

  case  7: k2 ^= tail[ 6] << 16;

  case  6: k2 ^= tail[ 5] << 8;

  case  5: k2 ^= tail[ 4] << 0;

           k2 *= c2; k2  = ROTL32(k2,16); k2 *= c3; h2 ^= k2;




  case  4: k1 ^= tail[ 3] << 24;

  case  3: k1 ^= tail[ 2] << 16;

  case  2: k1 ^= tail[ 1] << 8;

  case  1: k1 ^= tail[ 0] << 0;

           k1 *= c1; k1  = ROTL32(k1,15); k1 *= c2; h1 ^= k1;

  };




  h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len;




  h1 += h2; h1 += h3; h1 += h4;

  h2 += h1; h3 += h1; h4 += h1;




  h1 = fmix32(h1);

  h2 = fmix32(h2);

  h3 = fmix32(h3);

  h4 = fmix32(h4);




  h1 += h2; h1 += h3; h1 += h4;

  h2 += h1; h3 += h1; h4 += h1;




  ((uint32_t*)out)[0] = h1;

  ((uint32_t*)out)[1] = h2;

  ((uint32_t*)out)[2] = h3;

  ((uint32_t*)out)[3] = h4;

}




void MurmurHash3_x64_128 ( const void * key, const int len,

                           const uint32_t seed, void * out )

{

  const uint8_t * data = (const uint8_t*)key;

  const int nblocks = len / 16;

  int i;




  uint64_t h1 = seed;

  uint64_t h2 = seed;




  uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5);

  uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f);




  const uint64_t * blocks = (const uint64_t *)(data);




  for(i = 0; i < nblocks; i++)

  {

    uint64_t k1 = getblock(blocks,i*2+0);

    uint64_t k2 = getblock(blocks,i*2+1);




    k1 *= c1; k1  = ROTL64(k1,31); k1 *= c2; h1 ^= k1;




    h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;




    k2 *= c2; k2  = ROTL64(k2,33); k2 *= c1; h2 ^= k2;




    h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;

  }




  const uint8_t * tail = (const uint8_t*)(data + nblocks*16);




  uint64_t k1 = 0;

  uint64_t k2 = 0;




  switch(len & 15)

  {

  case 15: k2 ^= (uint64_t)(tail[14]) << 48;

  case 14: k2 ^= (uint64_t)(tail[13]) << 40;

  case 13: k2 ^= (uint64_t)(tail[12]) << 32;

  case 12: k2 ^= (uint64_t)(tail[11]) << 24;

  case 11: k2 ^= (uint64_t)(tail[10]) << 16;

  case 10: k2 ^= (uint64_t)(tail[ 9]) << 8;

  case  9: k2 ^= (uint64_t)(tail[ 8]) << 0;

           k2 *= c2; k2  = ROTL64(k2,33); k2 *= c1; h2 ^= k2;




  case  8: k1 ^= (uint64_t)(tail[ 7]) << 56;

  case  7: k1 ^= (uint64_t)(tail[ 6]) << 48;

  case  6: k1 ^= (uint64_t)(tail[ 5]) << 40;

  case  5: k1 ^= (uint64_t)(tail[ 4]) << 32;

  case  4: k1 ^= (uint64_t)(tail[ 3]) << 24;

  case  3: k1 ^= (uint64_t)(tail[ 2]) << 16;

  case  2: k1 ^= (uint64_t)(tail[ 1]) << 8;

  case  1: k1 ^= (uint64_t)(tail[ 0]) << 0;

           k1 *= c1; k1  = ROTL64(k1,31); k1 *= c2; h1 ^= k1;

  };







  h1 ^= len; h2 ^= len;




  h1 += h2;

  h2 += h1;




  h1 = fmix64(h1);

  h2 = fmix64(h2);




  h1 += h2;

  h2 += h1;




  ((uint64_t*)out)[0] = h1;

  ((uint64_t*)out)[1] = h2;

}

Before I run the program, I check who is logged in to AArchie by using the “who” command. There are two users logged in, so it should not affect my program performance. I use the “top” command to view running processes and I only see my processes, so that is good. Now, I compile this program using the same options as in the Makefile by using the command “gcc -Wall -Werror -g3 -O2 -Wstrict-aliasing=2 benchmark.c -o benchmark”. I will run this program ten times and get the average time to produce more accurate results. I run this program and here are the results:

[cslam4@aarchie include]$ ./benchmark

x86_32:  a005988f

Total time: 7.189417 seconds

x86_128: ead5c42f 18504ee9 0f36d6f8 8a268189

Total time: 6.251050 seconds

x64_128: fafd75f5 b84021d9 157f69c4 c52090ae

Total time: 5.295304 seconds

[cslam4@aarchie include]$ ./benchmark

x86_32:  a005988f

Total time: 7.191669 seconds

x86_128: ead5c42f 18504ee9 0f36d6f8 8a268189

Total time: 6.254057 seconds

x64_128: fafd75f5 b84021d9 157f69c4 c52090ae

Total time: 5.289192 seconds

[cslam4@aarchie include]$ ./benchmark

x86_32:  a005988f

Total time: 7.189152 seconds

x86_128: ead5c42f 18504ee9 0f36d6f8 8a268189

Total time: 6.252524 seconds

x64_128: fafd75f5 b84021d9 157f69c4 c52090ae

Total time: 5.289142 seconds

[cslam4@aarchie include]$ ./benchmark

x86_32:  a005988f

Total time: 7.190567 seconds

x86_128: ead5c42f 18504ee9 0f36d6f8 8a268189

Total time: 6.253532 seconds

x64_128: fafd75f5 b84021d9 157f69c4 c52090ae

Total time: 5.291871 seconds

[cslam4@aarchie include]$ ./benchmark

x86_32:  a005988f

Total time: 7.188331 seconds

x86_128: ead5c42f 18504ee9 0f36d6f8 8a268189

Total time: 6.249359 seconds

x64_128: fafd75f5 b84021d9 157f69c4 c52090ae

Total time: 5.291187 seconds

I only ran this program five times since it was producing very consistent results. The three hash functions produce different hashes due to different hash lengths and code that is optimized for a specific platform. However, it is good to see that the same hash has been produced for each function individually for all five runs. The average execution time for the first hash function is 7.1898 seconds. The average execution time for the second hash function is 6.2521 seconds. The average execution time for the third hash function is 5.2913 seconds.

Identify strategies to optimize hash function

After benchmarking the performance of the current hash functions, the next step is to identify strategies that I can use to optimize the current hash functions. I will start off with the easiest method, which is to alter the current build options for OlegDB. Currently, -O2 option is the only compiler option used to compile and build OlegDB. I will try to use other options such as -O3 option to enable more optimization flags or -Ofast option to enable all -O3 optimizations and optimizations that do not follow strict standards. I will also look at individual optimization flags and other options and perform tests to find out if there is a possible improvement in the performance of the hash functions. After looking at the build options, I will analyze the hash function code to search for ways to change the code so that the compiler can better optimize the function. This can include creating loops that can guide the compiler to allow for vectorization and the use of SIMD operations. It can also be changing code by replacing an expensive operation with a cheaper one or changing a loop iteration to make the loop run more efficiently. The third approach is to improve on existing algorithms. This will most likely not happen since the hash function implementation is already optimized for x86 or x64 systems, bit-shift operation is being used and other operations seem to be using the best algorithm possible. The last method is to use inline assembly language, which is also unlikely to happen since writing assembly code is difficult and I do not have enough knowledge to understand the complexity of inline assembly code. As a final note, I have been working with three hash functions instead of one because the OlegDB source code has already optimized the hash function implementation for x86 and x64 systems. The three hash functions are integrated into one file, so it is easier to leave it alone than to extract one specific function for optimization. Also, I can work with all three hash functions to increase my chance of finding optimization opportunities. Now that stage one of this project is complete, I can move on to stage two, which is to implement optimizations to the hash function.

SPO600 Project – Stage 1

For the first stage of this project, I need to find an open source software package that contains a checksum or hash function that is implemented in a language that compiles to machine code such as C. After finding the software, I need to benchmark the performance of the hash function on an AArch64 system. Lastly, I need to identify a few possible methods that I can use to try to optimize the hash function to improve performance.

Search for an open source software with hash function

The first step is to find an open source software. I have spent a few hours searching for software that contains a hash function that I can use for optimization. It turns out to be a difficult task. The problem is the software usually does not mention whether or not it uses a hash function, so I need to search the source code to find out, which is time consuming. I try to locate the hash function by using the “grep -inr” command to search for all of the files for a particular software that contain the string “hash” or “murmur” or “sha”. I see that some software such as AIDE and GPG support hash algorithms but I am unable to find a hash function in the source code. There is a lot of software such as pass that are not written in C and are written in other languages such as Java and bash, which means that I cannot use. In the end, I am able to find a hash function in OlegDB, which is a database software. OlegDB uses MurmurHash3 hashing algorithm to hash and index keys. Now that I have a software to work with, I will need to benchmark the performance of the hash function and come up with a plan to optimize the hash function, which will be discussed in my next post.

SPO600 Lab 6 Part 2 – Algorithm Selection

After performing all of the tests on Aarchie, I will now do the same on Xerxes to compare the relative performance of the three methods. I compile my program with no optimization and here are the results for all three methods after running the time command:

[cslam4@xerxes lab6]$ gcc lab6a.c -o lab6a
[cslam4@xerxes lab6]$ time ./lab6a
Sum of scaled samples: -115014951
CPU time used to scale samples: 3.730427 seconds

real      0m14.264s
user     0m13.106s
sys       0m1.145s
[cslam4@xerxes lab6]$ gcc lab6b.c -o lab6b
[cslam4@xerxes lab6]$ time ./lab6b
Sum of scaled samples: -115014951
CPU time used to scale samples: 3.501012 seconds

real      0m14.014s
user     0m12.797s
sys       0m1.202s
[cslam4@xerxes lab6]$ gcc lab6c.c -o lab6c
[cslam4@xerxes lab6]$ time ./lab6c
Sum of scaled samples: -302496613
CPU time used to scale samples: 2.712908 seconds

real      0m13.249s
user     0m12.055s
sys       0m1.179s

The sum of all scaled samples is -115014951 for the first two methods and is -302496613 for the third method, which is the same as the results on Aarchie. The first method took 3.730427 seconds to calculate the scaled sample values and store them in another array. The second method took about 6% less time than the first method. The third method took about 27% less time than the first method. For the first method, the real time is 14.264 seconds, the user time is 13.106 seconds and the system time is 1.145 seconds. For the second method, the real time, user time and system time are shorter than the first method. For the third method, the real time, user time and system time are shorter than the first two methods.

Now, I compile my program using -O3 option, which enables a lot of optimization. I perform the same tests above and here are the results for all three methods:

[cslam4@xerxes lab6]$ gcc -O3 lab6a.c -o lab6a
[cslam4@xerxes lab6]$ time ./lab6a
Sum of scaled samples: -115014951
CPU time used to scale samples: 1.587858 seconds

real      0m10.103s
user     0m8.908s
sys       0m1.184s
[cslam4@xerxes lab6]$ gcc -O3 lab6b.c -o lab6b
[cslam4@xerxes lab6]$ time ./lab6b
Sum of scaled samples: -115014951
CPU time used to scale samples: 1.729359 seconds

real      0m10.466s
user     0m9.237s
sys       0m1.219s
[cslam4@xerxes lab6]$ gcc -O3 lab6c.c -o lab6c
[cslam4@xerxes lab6]$ time ./lab6c
Sum of scaled samples: -302496613
CPU time used to scale samples: 1.088714 seconds

real      0m9.609s
user     0m8.451s
sys       0m1.147s

The sum of all scaled samples does not change after using -O3 option. Interestingly, the second method took about 9% more time than the first method to calculate the scaled sample values and store them in another array. For the third method, it is about 46% faster than the first method and 59% faster than the second method. The third method has the shortest real time, user time and system time while the second method has the longest real time, user time and system time. After optimization is enabled using -O3 option, the processing time has been drastically reduced for all three methods. When I compare to no optimization, the first method is 135% faster, the second method is 102% faster and the third method is 149% faster.

Now, I use “/usr/bin/time -v” to find out how much memory is used to run my program. Here are the results for all three methods with no optimization during compilation:

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6a
Sum of scaled samples: -115014951
CPU time used to scale samples: 3.212162 seconds
            Command being timed: "./lab6a"
            User time (seconds): 12.59
            System time (seconds): 1.13
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.75
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954152
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488338
            Voluntary context switches: 1
            Involuntary context switches: 1323
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6b
Sum of scaled samples: -115014951
CPU time used to scale samples: 3.605827 seconds
            Command being timed: "./lab6b"
            User time (seconds): 12.95
            System time (seconds): 1.17
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:14.15
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954044
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488369
            Voluntary context switches: 1
            Involuntary context switches: 1409
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6c
Sum of scaled samples: -302496613
CPU time used to scale samples: 2.712172 seconds
            Command being timed: "./lab6c"
            User time (seconds): 12.07
            System time (seconds): 1.16
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.25
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954152
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488338
            Voluntary context switches: 1
            Involuntary context switches: 1334
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

The results show that the first method and third method have the highest peak memory usage (1954152 kilobytes) and the second method has the lowest peak memory usage (1954044 kilobytes). However, the difference in peak memory usage is very small, so it is insignificant.

Here are the results for all three methods using -O3 option during compilation:

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6a
Sum of scaled samples: -115014951
CPU time used to scale samples: 1.584637 seconds
            Command being timed: "./lab6a"
            User time (seconds): 8.90
            System time (seconds): 1.17
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:10.09
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954204
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488341
            Voluntary context switches: 1
            Involuntary context switches: 968
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6b
Sum of scaled samples: -115014951
CPU time used to scale samples: 1.724290 seconds
            Command being timed: "./lab6b"
            User time (seconds): 9.21
            System time (seconds): 1.23
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:10.45
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954104
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488369
            Voluntary context switches: 1
            Involuntary context switches: 1003
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

[cslam4@xerxes lab6]$ /usr/bin/time -v ./lab6c
Sum of scaled samples: -302496613
CPU time used to scale samples: 1.088512 seconds
            Command being timed: "./lab6c"
            User time (seconds): 8.44
            System time (seconds): 1.14
            Percent of CPU this job got: 99%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:09.60
            Average shared text size (kbytes): 0
            Average unshared data size (kbytes): 0
            Average stack size (kbytes): 0
            Average total size (kbytes): 0
            Maximum resident set size (kbytes): 1954196
            Average resident set size (kbytes): 0
            Major (requiring I/O) page faults: 0
            Minor (reclaiming a frame) page faults: 488338
            Voluntary context switches: 1
            Involuntary context switches: 965
            Swaps: 0
            File system inputs: 0
            File system outputs: 0
            Socket messages sent: 0
            Socket messages received: 0
            Signals delivered: 0
            Page size (bytes): 4096
            Exit status: 0

The results show that the first method has the highest peak memory usage (1954204 kilobytes) and the second method has the lowest peak memory usage (1954104 kilobytes). Peak memory usage is pretty much the same as the case with no optimization enabled.

Conclusion

From the results above, we see that the third method generates the shortest CPU processing time, which is also true on Aarchie. The first method is slower than the second method without optimization but the first method is faster than the second method with optimization. We get the same results when we compare the three methods using the real time and user time from the time command. This is different from the results on Aarchie where the first method is faster than the second method regardless of whether a lot of optimization is enabled. The results still show that the third method is the adjusting volume approach that results in the best performance. In general, the results on Xerxes are similar to the results on Aarchie.

SPO600 Lab 6 – Algorithm Selection

Select algorithm to adjust volume of PCM audio samples

Digital sound is usually represented as signed 16-bit integer signal samples. If we want to change the volume of sound, we need to scale each sample by a volume factor. I will use three methods to scale samples and then test and see which method requires the least CPU processing time.

For testing, I will create a 500 million element array of int16_t numbers to represent sound samples. I scale each sample by a volume factor of 0.75 and store the results in another array. I sum up all the results and display the total.

For the first method, I simply multiply each sample by the floating point volume factor 0.75 to get the scaled sample value. Here is my script:

lab6script1

The clock function is used to determine the amount of CPU processing time that is used to calculate the scaled sample values and store them in another array. I will first run all my tests on Aarchie. I compile my program with no optimization. I use the time command to determine how long it takes to run my program. The time command shows the real time, the user CPU time and the system CPU time. Here is the result:

lab6a1

The sum of all scaled samples is -115014951. It took 4.648345 seconds to calculate the scaled sample values and store them in another array. The real time is 30.631 seconds, the user time is 29.512 seconds and the system time is 1.099 seconds.

For the second method, I pre-calculate a lookup table (array) that contains all possible sample values multiplied by the volume factor 0.75 and look up each sample in that table to get the corresponding scaled sample value. Here is my script:

lab6script2

Here is the result from the time command:

lab6b1

The sum of all scaled samples is -115014951, which is the same as the first method. It took 5.107905 seconds to calculate the scaled sample values and store them in another array, which is about 10% more time than the first method. The real time, user time and system time are also longer than the first method.

For the third method, I convert the volume factor 0.75 to a fix-point integer by multiplying it by the binary number 0b100000000, which is 256 in decimal. I multiply this result by each sample value and shift the result to the right by 8 bits to get the scaled sample value. Here is my script:

lab6script3

Here is the result from the time command:

lab6c1

The sum of all scaled samples is -302496613, which is different from the first two methods. This is because the scaled sample values are slightly off due to conversions during the calculation. It took 4.068071 seconds to calculate the scaled sample values and store them in another array, which is about 14% faster than the first method and 26% faster than the second method. The real time and user time are also shorter than the first two methods.

Now, I compile my program using -O3 option, which enables a lot of optimization. I perform the same tests above. Here are the results for all three methods:

lab6a3

The sum of all scaled samples does not change after using -O3 option. For the second method, it took about 61% more time than the first method to calculate the scaled sample values and store them in another array. For the third method, it is about 80% (almost 2 times) faster than the first method and 189% (almost 3 times) faster than the second method. The third method has the shortest real time and user time and the second method has the longest real time and user time, which are the same results as before. After optimization is enabled using -O3 option, the processing time has been drastically reduced for all three methods. When I compare to no optimization, the first method is 245% (about 3.5 times) faster, the second method is 136% (about 2.4 times) faster and the third method is 443% (about 5.4 times) faster.

Now, I will use “/usr/bin/time -v” to find out how much memory is used to run my program. Here are the results for all three methods with no optimization during compilation:

lab6a4lab6b4lab6c4

I look for “maximum resident set size”, which is the peak memory usage. Based on the results above, the third method has the highest peak memory usage (1954068 kilobytes) and the second method has the lowest peak memory usage (1953960 kilobytes). However, the difference in peak memory usage is very small, so it is insignificant.

Here are the results for all three methods using -O3 option during compilation:

lab6a5lab6b5lab6c5

Based on the results above, the third method has the highest peak memory usage (1954060 kilobytes) and the second method has the lowest peak memory usage (1953980 kilobytes). Peak memory usage is pretty much the same as the case with no optimization enabled.

Conclusion

From the results above, we see that the third method generates the shortest CPU processing time and the second method generates the longest CPU processing time regardless of whether or not optimization is enabled when compiling my program. We get the same results when we compare the three methods using the real time and user time from the time command. This means that the approach to adjust volume that results in the best performance is the third method. The third method converts the volume factor to a fix-point integer by multiplying it by a binary number representing a fixed-point value “1”, multiplies this result by each sample value and shifts the result to the right by the correct number of bits to get the scaled sample value.

The results also show that the CPU processing time is significantly reduced for all three methods after optimization is enabled using -O3 option. This means that the performance of my program is significantly enhanced after enabling optimization. It takes at most a few seconds to scale 500 million samples for all three methods, so the speed is much quicker than the typical sample rate of 88200 samples per second.

In terms of memory footprint, the results show that the peak memory usage is almost 2 gigabytes when I am running my program. Peak memory usage is about the same for all three methods and regardless of whether or not optimization is enabled. I can run the command “ps aux” to find out how much memory is used for currently running processes.

In order to calculate the amount of energy consumed by each approach, I need to measure the amount of hardware resources that are used such as CPU, memory and disk usage and then convert this usage into power usage using some kind of software. The third method will consume the least energy and less energy will be consumed when optimization is enabled due to faster code and shorter CPU processing time.

After performing all of the tests above on Aarchie, I need to do the same on Xerxes. However, there is no free space on Xerxes, so I am unable to perform these tests on Xerxes to compare the relative performance of the three methods. I predict that the result will be similar on Xerxes and the third method has the best performance.

I have used simple loops in my script in order to further optimize my program. When I use -O3 option to compile my program, it enables auto-vectorization. With -O3 option, the loops can be vectorized and SIMD instructions are used, which will reduce CPU processing time and improve performance. Another possible method to optimize my program is to use an integer volume factor instead of floating point in order to avoid floating-point calculations.

SPO600 Lab 5 – SIMD and Auto-Vectorization

SIMD instructions and vectorization

Vectorization refers to a compiler unrolling a loop combined with generating SIMD instructions. Each SIMD (Single Instruction Multiple Data) instruction operates on more than one data element at a time, so a loop can run more efficiently. With auto-vectorization, the compiler can identify and optimize some loops on its own, which means it can automatically vectorize a loop. Aarch64 has 32 128-bit wide vector registers that SIMD instructions use and they are named V0 to V31. You can refer to the ARM manual for more information about SIMD instructions and vector registers.

Writing vectorizable code and enabling auto-vectorization

For this lab, I need to write a program that fills two 1000-element integer arrays with random numbers between -1000 and 1000, sums these two arrays element-by-element to a third array, and calculates the sum of all elements in the third array and prints the result. Here is my program that accomplishes these tasks without considering vectorization:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define RANDNUM 1000

int main(void)
{
 // Declare variables
 int array1[RANDNUM], array2[RANDNUM], array3[RANDNUM];
 int i, minNum = -1000, maxNum = 1000, sum = 0;

// Randomize seed
 srand(time(NULL));

for (i = 0; i < RANDNUM; i++) {
 // Store random numbers in two arrays
 array1[i] = minNum + rand() % (maxNum + 1 - minNum);
 array2[i] = minNum + rand() % (maxNum + 1 - minNum);

// Sum array elements into third array
 array3[i] = array1[i] + array2[i];

// Sum of third array elements
 sum += array3[i];
 }

// Display sum of third array elements
 printf("Sum of all elements in the third array is: %d\n", sum);
 return 0;
}

I use the command “gcc -O0 lab5.c -o lab5” to compile my program with no optimization using the -O0 option. Here is the disassembly output for the section <main> using the “objdump -d” command:

0000000000400684 <main>:
 400684: d285e010 mov x16, #0x2f00 // #12032
 400688: cb3063ff sub sp, sp, x16
 40068c: a9007bfd stp x29, x30, [sp]
 400690: 910003fd mov x29, sp
 400694: 12807ce0 mov w0, #0xfffffc18 // #-1000
 400698: b92ef7a0 str w0, [x29,#12020]
 40069c: 52807d00 mov w0, #0x3e8 // #1000
 4006a0: b92ef3a0 str w0, [x29,#12016]
 4006a4: b92efbbf str wzr, [x29,#12024]
 4006a8: d2800000 mov x0, #0x0 // #0
 4006ac: 97ffff99 bl 400510 <time@plt>
 4006b0: 97ffffac bl 400560 <srand@plt>
 4006b4: b92effbf str wzr, [x29,#12028]
 4006b8: 14000038 b 400798 <main+0x114>
 4006bc: 97ffff9d bl 400530 <rand@plt>
 4006c0: 2a0003e1 mov w1, w0
 4006c4: b96ef3a0 ldr w0, [x29,#12016]
 4006c8: 11000402 add w2, w0, #0x1
 4006cc: b96ef7a0 ldr w0, [x29,#12020]
 4006d0: 4b000040 sub w0, w2, w0
 4006d4: 1ac00c22 sdiv w2, w1, w0
 4006d8: 1b007c40 mul w0, w2, w0
 4006dc: 4b000021 sub w1, w1, w0
 4006e0: b96ef7a0 ldr w0, [x29,#12020]
 4006e4: 0b000022 add w2, w1, w0
 4006e8: b9aeffa0 ldrsw x0, [x29,#12028]
 4006ec: d37ef400 lsl x0, x0, #2
 4006f0: 914007a1 add x1, x29, #0x1, lsl #12
 4006f4: 913d4021 add x1, x1, #0xf50
 4006f8: b8206822 str w2, [x1,x0]
 4006fc: 97ffff8d bl 400530 <rand@plt>
 400700: 2a0003e1 mov w1, w0
 400704: b96ef3a0 ldr w0, [x29,#12016]
 400708: 11000402 add w2, w0, #0x1
 40070c: b96ef7a0 ldr w0, [x29,#12020]
 400710: 4b000040 sub w0, w2, w0
 400714: 1ac00c22 sdiv w2, w1, w0
 400718: 1b007c40 mul w0, w2, w0
 40071c: 4b000021 sub w1, w1, w0
 400720: b96ef7a0 ldr w0, [x29,#12020]
 400724: 0b000022 add w2, w1, w0
 400728: b9aeffa0 ldrsw x0, [x29,#12028]
 40072c: d37ef400 lsl x0, x0, #2
 400730: 913ec3a1 add x1, x29, #0xfb0
 400734: b8206822 str w2, [x1,x0]
 400738: b9aeffa0 ldrsw x0, [x29,#12028]
 40073c: d37ef400 lsl x0, x0, #2
 400740: 914007a1 add x1, x29, #0x1, lsl #12
 400744: 913d4021 add x1, x1, #0xf50
 400748: b8606821 ldr w1, [x1,x0]
 40074c: b9aeffa0 ldrsw x0, [x29,#12028]
 400750: d37ef400 lsl x0, x0, #2
 400754: 913ec3a2 add x2, x29, #0xfb0
 400758: b8606840 ldr w0, [x2,x0]
 40075c: 0b000022 add w2, w1, w0
 400760: b9aeffa0 ldrsw x0, [x29,#12028]
 400764: d37ef400 lsl x0, x0, #2
 400768: 910043a1 add x1, x29, #0x10
 40076c: b8206822 str w2, [x1,x0]
 400770: b9aeffa0 ldrsw x0, [x29,#12028]
 400774: d37ef400 lsl x0, x0, #2
 400778: 910043a1 add x1, x29, #0x10
 40077c: b8606820 ldr w0, [x1,x0]
 400780: b96efba1 ldr w1, [x29,#12024]
 400784: 0b000020 add w0, w1, w0
 400788: b92efba0 str w0, [x29,#12024]
 40078c: b96effa0 ldr w0, [x29,#12028]
 400790: 11000400 add w0, w0, #0x1
 400794: b92effa0 str w0, [x29,#12028]
 400798: b96effa0 ldr w0, [x29,#12028]
 40079c: 710f9c1f cmp w0, #0x3e7
 4007a0: 54fff8ed b.le 4006bc <main+0x38>
 4007a4: 90000000 adrp x0, 400000 <_init-0x4d8>
 4007a8: 91220000 add x0, x0, #0x880
 4007ac: b96efba1 ldr w1, [x29,#12024]
 4007b0: 97ffff70 bl 400570 <printf@plt>
 4007b4: 52800000 mov w0, #0x0 // #0
 4007b8: a9407bfd ldp x29, x30, [sp]
 4007bc: d285e010 mov x16, #0x2f00 // #12032
 4007c0: 8b3063ff add sp, sp, x16
 4007c4: d65f03c0 ret

The disassembly output above contains 81 lines of instructions.

Now, I use the command “gcc -O3 lab5.c -o lab5a” to compile my program with a lot of optimization using the -O3 option. The -O3 option enables a lot of optimization and enables auto-vectorization. Here is the disassembly output for the section <main>:

0000000000400580 <main>:
 400580: a9bc7bfd stp x29, x30, [sp,#-64]!
 400584: d2800000 mov x0, #0x0 // #0
 400588: 910003fd mov x29, sp
 40058c: a9025bf5 stp x21, x22, [sp,#32]
 400590: 529a9c75 mov w21, #0xd4e3 // #54499
 400594: a90153f3 stp x19, x20, [sp,#16]
 400598: 72a83015 movk w21, #0x4180, lsl #16
 40059c: f9001bf7 str x23, [sp,#48]
 4005a0: 52807d13 mov w19, #0x3e8 // #1000
 4005a4: 5280fa34 mov w20, #0x7d1 // #2001
 4005a8: 52800017 mov w23, #0x0 // #0
 4005ac: 97ffffd9 bl 400510 <time@plt>
 4005b0: 97ffffec bl 400560 <srand@plt>
 4005b4: 97ffffdf bl 400530 <rand@plt>
 4005b8: 2a0003f6 mov w22, w0
 4005bc: 97ffffdd bl 400530 <rand@plt>
 4005c0: 9b357c03 smull x3, w0, w21
 4005c4: 71000673 subs w19, w19, #0x1
 4005c8: 9b357ec2 smull x2, w22, w21
 4005cc: 9369fc63 asr x3, x3, #41
 4005d0: 4b807c63 sub w3, w3, w0, asr #31
 4005d4: 9369fc42 asr x2, x2, #41
 4005d8: 4b967c42 sub w2, w2, w22, asr #31
 4005dc: 1b148060 msub w0, w3, w20, w0
 4005e0: 1b14d842 msub w2, w2, w20, w22
 4005e4: 0b000040 add w0, w2, w0
 4005e8: 511f4000 sub w0, w0, #0x7d0
 4005ec: 0b0002f7 add w23, w23, w0
 4005f0: 54fffe21 b.ne 4005b4 <main+0x34>
 4005f4: 2a1703e1 mov w1, w23
 4005f8: 90000000 adrp x0, 400000 <_init-0x4d8>
 4005fc: 911f8000 add x0, x0, #0x7e0
 400600: 97ffffdc bl 400570 <printf@plt>
 400604: 52800000 mov w0, #0x0 // #0
 400608: f9401bf7 ldr x23, [sp,#48]
 40060c: a94153f3 ldp x19, x20, [sp,#16]
 400610: a9425bf5 ldp x21, x22, [sp,#32]
 400614: a8c47bfd ldp x29, x30, [sp],#64
 400618: d65f03c0 ret
 40061c: 00000000 .inst 0x00000000 ; undefined

The disassembly output above contains 40 lines of instructions, which is about half the amount of instructions compared to the first case. This is an indication that optimization has occurred. Auto-vectorization is enabled but the disassembly output does not contain SIMD instructions, which means that the code is not vectorized.

I need to change my code in order for it to become vectorizable. Instead of using one for loop,  I will divide it into three for loops. The first loop stores random numbers into the two arrays. The second loop sums these two arrays element-by-element to a third array. The third loop calculates the sum of all of the elements in the third array. Here is my program with vectorizable code:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define RANDNUM 1000

int main(void)
{
 // Declare variables
 int array1[RANDNUM], array2[RANDNUM], array3[RANDNUM];
 int i, minNum = -1000, maxNum = 1000, sum = 0;

// Randomize seed
 srand(time(NULL));

// Store random numbers in two arrays
 for (i = 0; i < RANDNUM; i++) {
 array1[i] = minNum + rand() % (maxNum + 1 - minNum);
 array2[i] = minNum + rand() % (maxNum + 1 - minNum);
 }

// Sum array elements into third array
 for (i = 0; i < RANDNUM; i++) {
 array3[i] = array1[i] + array2[i];
 }

// Sum of third array elements
 for (i = 0; i < RANDNUM; i++) {
 sum += array3[i];
 }

// Display sum of third array elements
 printf("Sum of all elements in the third array is: %d\n", sum);
 return 0;

I use the command “gcc -O0 lab5b.c -o lab5b” to compile my program with no optimization using the -O0 option. Here is the disassembly output for the section <main>:

0000000000400684 <main>:
 400684: d285e010 mov x16, #0x2f00 // #12032
 400688: cb3063ff sub sp, sp, x16
 40068c: a9007bfd stp x29, x30, [sp]
 400690: 910003fd mov x29, sp
 400694: 12807ce0 mov w0, #0xfffffc18 // #-1000
 400698: b92ef7a0 str w0, [x29,#12020]
 40069c: 52807d00 mov w0, #0x3e8 // #1000
 4006a0: b92ef3a0 str w0, [x29,#12016]
 4006a4: b92efbbf str wzr, [x29,#12024]
 4006a8: d2800000 mov x0, #0x0 // #0
 4006ac: 97ffff99 bl 400510 <time@plt>
 4006b0: 97ffffac bl 400560 <srand@plt>
 4006b4: b92effbf str wzr, [x29,#12028]
 4006b8: 14000023 b 400744 <main+0xc0>
 4006bc: 97ffff9d bl 400530 <rand@plt>
 4006c0: 2a0003e1 mov w1, w0
 4006c4: b96ef3a0 ldr w0, [x29,#12016]
 4006c8: 11000402 add w2, w0, #0x1
 4006cc: b96ef7a0 ldr w0, [x29,#12020]
 4006d0: 4b000040 sub w0, w2, w0
 4006d4: 1ac00c22 sdiv w2, w1, w0
 4006d8: 1b007c40 mul w0, w2, w0
 4006dc: 4b000021 sub w1, w1, w0
 4006e0: b96ef7a0 ldr w0, [x29,#12020]
 4006e4: 0b000022 add w2, w1, w0
 4006e8: b9aeffa0 ldrsw x0, [x29,#12028]
 4006ec: d37ef400 lsl x0, x0, #2
 4006f0: 914007a1 add x1, x29, #0x1, lsl #12
 4006f4: 913d4021 add x1, x1, #0xf50
 4006f8: b8206822 str w2, [x1,x0]
 4006fc: 97ffff8d bl 400530 <rand@plt>
 400700: 2a0003e1 mov w1, w0
 400704: b96ef3a0 ldr w0, [x29,#12016]
 400708: 11000402 add w2, w0, #0x1
 40070c: b96ef7a0 ldr w0, [x29,#12020]
 400710: 4b000040 sub w0, w2, w0
 400714: 1ac00c22 sdiv w2, w1, w0
 400718: 1b007c40 mul w0, w2, w0
 40071c: 4b000021 sub w1, w1, w0
 400720: b96ef7a0 ldr w0, [x29,#12020]
 400724: 0b000022 add w2, w1, w0
 400728: b9aeffa0 ldrsw x0, [x29,#12028]
 40072c: d37ef400 lsl x0, x0, #2
 400730: 913ec3a1 add x1, x29, #0xfb0
 400734: b8206822 str w2, [x1,x0]
 400738: b96effa0 ldr w0, [x29,#12028]
 40073c: 11000400 add w0, w0, #0x1
 400740: b92effa0 str w0, [x29,#12028]
 400744: b96effa0 ldr w0, [x29,#12028]
 400748: 710f9c1f cmp w0, #0x3e7
 40074c: 54fffb8d b.le 4006bc <main+0x38>
 400750: b92effbf str wzr, [x29,#12028]
 400754: 14000012 b 40079c <main+0x118>
 400758: b9aeffa0 ldrsw x0, [x29,#12028]
 40075c: d37ef400 lsl x0, x0, #2
 400760: 914007a1 add x1, x29, #0x1, lsl #12
 400764: 913d4021 add x1, x1, #0xf50
 400768: b8606821 ldr w1, [x1,x0]
 40076c: b9aeffa0 ldrsw x0, [x29,#12028]
 400770: d37ef400 lsl x0, x0, #2
 400774: 913ec3a2 add x2, x29, #0xfb0
 400778: b8606840 ldr w0, [x2,x0]
 40077c: 0b000022 add w2, w1, w0
 400780: b9aeffa0 ldrsw x0, [x29,#12028]
 400784: d37ef400 lsl x0, x0, #2
 400788: 910043a1 add x1, x29, #0x10
 40078c: b8206822 str w2, [x1,x0]
 400790: b96effa0 ldr w0, [x29,#12028]
 400794: 11000400 add w0, w0, #0x1
 400798: b92effa0 str w0, [x29,#12028]
 40079c: b96effa0 ldr w0, [x29,#12028]
 4007a0: 710f9c1f cmp w0, #0x3e7
 4007a4: 54fffdad b.le 400758 <main+0xd4>
 4007a8: b92effbf str wzr, [x29,#12028]
 4007ac: 1400000b b 4007d8 <main+0x154>
 4007b0: b9aeffa0 ldrsw x0, [x29,#12028]
 4007b4: d37ef400 lsl x0, x0, #2
 4007b8: 910043a1 add x1, x29, #0x10
 4007bc: b8606820 ldr w0, [x1,x0]
 4007c0: b96efba1 ldr w1, [x29,#12024]
 4007c4: 0b000020 add w0, w1, w0
 4007c8: b92efba0 str w0, [x29,#12024]
 4007cc: b96effa0 ldr w0, [x29,#12028]
 4007d0: 11000400 add w0, w0, #0x1
 4007d4: b92effa0 str w0, [x29,#12028]
 4007d8: b96effa0 ldr w0, [x29,#12028]
 4007dc: 710f9c1f cmp w0, #0x3e7
 4007e0: 54fffe8d b.le 4007b0 <main+0x12c>
 4007e4: 90000000 adrp x0, 400000 <_init-0x4d8>
 4007e8: 91230000 add x0, x0, #0x8c0
 4007ec: b96efba1 ldr w1, [x29,#12024]
 4007f0: 97ffff60 bl 400570 <printf@plt>
 4007f4: 52800000 mov w0, #0x0 // #0
 4007f8: a9407bfd ldp x29, x30, [sp]
 4007fc: d285e010 mov x16, #0x2f00 // #12032
 400800: 8b3063ff add sp, sp, x16
 400804: d65f03c0 ret

The disassembly output above contains 97 lines of instructions. We get more instructions than the first case with one loop, which is as expected since we now have three loops. Also as expected, the disassembly output does not contain SIMD instructions since auto-vectorization is not enabled.

Now, I use the command “gcc -O3 lab5b.c -o lab5c” to compile my program with a lot of optimization using the -O3 option. Here is the disassembly output with my bolded comments for the section <main>:

0000000000400580 <main>:
// main() function
 400580: d285e410 mov x16, #0x2f20 // #12064
 400584: cb3063ff sub sp, sp, x16 // stack pointer - x16
 400588: d2800000 mov x0, #0x0 // #0
 40058c: a9007bfd stp x29, x30, [sp] // store x29 and x30 to stack pointer address
 400590: 910003fd mov x29, sp // move stack pointer to x29
 400594: a90153f3 stp x19, x20, [sp,#16] // store x19 and x20 to stack pointer address with offset
 400598: 529a9c74 mov w20, #0xd4e3 // #54499
 40059c: a9025bf5 stp x21, x22, [sp,#32] // store x21 and x22 to stack pointer address with offset
 4005a0: 72a83014 movk w20, #0x4180, lsl #16 // move value to w20
 4005a4: f9001bf7 str x23, [sp,#48] // store x23 to stack pointer address with offset
 4005a8: 910103b6 add x22, x29, #0x40 // x29 + 64 and store in x22
 4005ac: 913f83b5 add x21, x29, #0xfe0 // x29 + 4064 and store in x21
 4005b0: 5280fa33 mov w19, #0x7d1 // #2001
 4005b4: d2800017 mov x23, #0x0 // #0
 4005b8: 97ffffd6 bl 400510 <time@plt> // call time subroutine
 4005bc: 97ffffe9 bl 400560 <srand@plt> // call srand subroutine
// first loop
// array1[i] = minNum + rand() % (maxNum + 1 - minNum)
 4005c0: 97ffffdc bl 400530 <rand@plt> // call rand subroutine
 4005c4: 9b347c01 smull x1, w0, w20 // w0 * w20 and store in x1
 4005c8: 9369fc21 asr x1, x1, #41 // shift x1 value right by 41 bits
 4005cc: 4b807c21 sub w1, w1, w0, asr #31 // subtract shifted register
 4005d0: 1b138020 msub w0, w1, w19, w0 // multiply and subtract
 4005d4: 510fa000 sub w0, w0, #0x3e8 // subtract
 4005d8: b8376ac0 str w0, [x22,x23] // store w0 to an address
// array2[i] = minNum + rand() % (maxNum + 1 - minNum)
 4005dc: 97ffffd5 bl 400530 <rand@plt> // call rand subroutine
 4005e0: 9b347c01 smull x1, w0, w20 // w0 * w20 and store in x1
 4005e4: 9369fc21 asr x1, x1, #41 // shift x1 value right by 41 bits
 4005e8: 4b807c21 sub w1, w1, w0, asr #31 // subtract shifted register
 4005ec: 1b138020 msub w0, w1, w19, w0 // multiply and subtract
 4005f0: 510fa000 sub w0, w0, #0x3e8 // subtract
 4005f4: b8376aa0 str w0, [x21,x23] // store w0 to an address
// loop if i < RANDNUM
 4005f8: 910012f7 add x23, x23, #0x4 // x23 + 4 and store in x23
 4005fc: f13e82ff cmp x23, #0xfa0 // test if x23 = 4000
 400600: 54fffe01 b.ne 4005c0 <main+0x40> // repeat first loop if x23 not equal 4000
 400604: d283f002 mov x2, #0x1f80 // #8064
 400608: 8b0203a1 add x1, x29, x2 // x29 + x2 and store in x1
 40060c: d2800000 mov x0, #0x0 // #0
// second loop
// array3[i] = array1[i] + array2[i];
 400610: 3ce06ac0 ldr q0, [x22,x0] // load register
 400614: 3ce06aa1 ldr q1, [x21,x0] // load register
 400618: 4ea18400 add v0.4s, v0.4s, v1.4s // SIMD vector instruction: v0.4s + v1.4s and store in v0.4s
 40061c: 3ca06820 str q0, [x1,x0] // store q0 to an address
// loop if i < RANDNUM
 400620: 91004000 add x0, x0, #0x10 // x0 + 16 and store in x0
 400624: f13e801f cmp x0, #0xfa0 // test if x0 = 4000
 400628: 54ffff41 b.ne 400610 <main+0x90> // repeat second loop if x0 not equal 4000
 40062c: 4f000400 movi v0.4s, #0x0 // SIMD vector instruction: move immediate (vector)
 400630: aa0103e0 mov x0, x1 // move x1 to x29
 400634: d285e401 mov x1, #0x2f20 // #12064
 400638: 8b0103a1 add x1, x29, x1 // x29 + x1 and store in x1
// third loop
// sum += array3[i];
 40063c: 3cc10401 ldr q1, [x0],#16 // load register
 400640: 4ea18400 add v0.4s, v0.4s, v1.4s // SIMD vector instruction: v0.4s + v1.4s and store in v0.4s
 400644: eb01001f cmp x0, x1 // test if x0 = x1
 400648: 54ffffa1 b.ne 40063c <main+0xbc> // repeat third loop if x0 not equal x1
 40064c: 4eb1b800 addv s0, v0.4s // SIMD vector instruction: add across vector
 400650: 90000000 adrp x0, 400000 <_init-0x4d8> // store address in x0
 400654: 91210000 add x0, x0, #0x840 // x0 + 2112 and store in x0
 400658: 0e043c01 mov w1, v0.s[0] // SIMD vector instruction: move v0.s[0] to w1
 40065c: 97ffffc5 bl 400570 <printf@plt> // call printf subroutine
 400660: f9401bf7 ldr x23, [sp,#48] // load register
 400664: a94153f3 ldp x19, x20, [sp,#16] // load pair of registers
 400668: 52800000 mov w0, #0x0 // #0
 40066c: a9425bf5 ldp x21, x22, [sp,#32] // load pair of registers
 400670: d285e410 mov x16, #0x2f20 // #12064
 400674: a9407bfd ldp x29, x30, [sp] // load pair of registers
 400678: 8b3063ff add sp, sp, x16 // stack pointer + x16 and store in stack pointer
 40067c: d65f03c0 ret // return from subroutine

The disassembly output above contains 64 lines of instructions, which is less than the case with no optimization. In this case, the disassembly output contains SIMD instructions, which means that the code is vectorized. Specifically, the disassembly output shows that the second and third loop is vectorized. The second and third loop contains a few SIMD vector instructions where vector registers are used. For example, the SIMD instruction “add v0.4s, v0.4s, v1.4s” allows 4 additions to be performed in a single instruction. In terms of register “v0.4s”, “v0” represents vector register 0, “4” represents 4 data elements or lanes, and “s” represents the data element size of 32 bits. One instruction uses “v0.s[0]”, which represents a vector register element where “[0]” indicates the element index. Some SIMD instructions use the same name as other types of instructions. For example, we have “add” and “mov” instructions that become SIMD instructions when vector registers are used.

There are a few things to consider when you want to write vectorizable loops. Simple loops are more likely to be vectorizable than complex loops. A loop will not be vectorizable if it contains complex calculations such as the first loop in my program. This is also true if data dependencies exist within the loop, which is when the value of one variable depends on the value of another variable and values are overwritten. These three conditions explain why my first program that has only one big loop cannot be vectorized. Writing vectorizable code is not easy because different compilers handle vectorization differently and we are unfamiliar with that process. It will probably take at least a couple of attempts in modifying our code to get it to work. There are some general guidelines that we can follow but these guidelines may not be always helpful. On the other hand, it is not difficult to identify vectorized code that is shown in the disassembly output.

SPO600 Lab 4 – Build and Test Open Source Software

Build and test an open source software

For the first part of this lab, I need to build and test an open source software. I will be using Fedora to build and test open source software. I need to choose an open source software package from an open source project. I have decided to choose the grep package from the GNU Project. Grep is used to search for lines in a file that match a specified pattern and then outputs these lines. Here are the steps to compile and test grep:

  1. From a local directory (eg. ~/spo600/lab4), download the grep package by issuing the command “wget https://ftp.gnu.org/gnu/grep/grep-3.1.tar.xz&#8221; in the command line. We need to find out the URL for the grep package before using the wget command.
  2. Issue the command “tar xvf grep-3.1.tar.xz” to extract and uncompress the package to the folder “grep-3.1”.
  3. The next step is to figure out how to build the software. After the package has been extracted, we see a lot of files under the main folder “grep-3.1”. There is a “README” file, so we issue the command “cat README” to view the contents of this file. This file tells us general information about grep as well as important files and their uses. It tells us that the “INSTALL” file contains information about compilation and installation, so we issue the command “cat INSTALL” to view the contents of this file. The “INSTALL” file provides detailed instructions on how to compile and install the software. For this lab, we will not be installing the software, so we can ignore sections about software installation. This file tells us that the command “make check” is used to run self-tests that are included with the package. This file also tells us that it takes two steps to compile the package. The first step is to use the “cd” command to move to the directory that contains the “configure” file and then issue the command “./configure”. This will run the shell script to configure the package and create the “Makefile” file based on my system information. The second step is to issue the command “make” to compile the package using the “Makefile” file. Now that we have read the “INSTALL” file, we can issue the command “./configure” from the directory “grep-3.1” (eg. ~/spo600/lab4/grep-3.1) to configure the package and create the “Makefile” file for my system:

grep1

  1. After “configure” runs successfully, we use the command “make” to compile grep:

grep2

  1. After the compilation has completed, we are ready to test our grep build. We issue the command “make check” to run the test scripts that are provided with the package and we see that most tests are successful:

grep3

Next, we will try to use the grep utility that we have built. We issue the command “cd src” to move to the “src” directory, which is where the grep executable from our build is located. We then issue the command “./grep the dfasearch.c” to run our version of grep. This grep command will search for all lines in the file “dfasearch.c” that contain the string “the” and display the results on the screen. It seems like we get the correct results, so our grep build works:

grep4

I have grep installed on my system, so let’s use the installed version of grep and see what results we get. We issue the command “grep the dfasearch.c” to run the installed version of grep. We get the same results as above, which proves that the results from our version of grep are correct and we have built grep successfully. The only difference in terms of output is that this time the search string “the” is highlighted in red in the output:

grep5

Here, we see that the compilation process is fairly straightforward. We download and extract the software package that we want and then run a couple of commands to compile the software. We have not been asked to install any dependencies during the compilation process. We just need to read the “INSTALL” file to obtain instructions on how to compile and install the software.

Build and test glibc

For the second part of this lab, I need to build and test GNU C Library (glibc). Here are the steps to compile and test glibc:

  1. We issue the command “mkdir ~/spo600/lab4/src” to create the directory “src” to store the source code.
  2. Issue the command “cd ~/spo600/lab4/src” to move to the “src” directory.
  3. Issue the command “git clone git://sourceware.org/git/glibc.git” to download the glibc source code to the “src” directory.
  4. As I already mentioned, the “INSTALL” file provides instructions for compilation and installation. We need to issue the command “cd glibc” to move to the “glibc” directory and then issue the command “cat INSTALL” to view the contents of this file. It tells us that the GNU C Library cannot be compiled in the source directory. We need to build it in another directory, so we will create a build directory. Same as grep, the file tells us to run the “configure” script, use “make” to compile glibc and then use “make check” to test glibc. The file also tells us that we should install and update the following tools before building the GNU C Library: make, GCC, binutils, texinfo, gawk, Perl, and sed. In addition, we need to install and update the following tools before we can run test scripts after building the GNU C Library: Python, PExpect and GDB.
  5. Use the “sudo yum install” and “sudo yum update” commands to install and update all of the tools listed above.
  6. In the end, I need to run a program to verify that I am testing the newly built glibc. Therefore, I will introduce a small change to the source code for a function in order to differentiate between the new glibc and system glibc. I decide to change the code for the rand() function so that the function returns the number 5 instead of a random number. The path to the rand() function source file is “~/spo600/lab4/src/glibc/stdlib/rand.c” and here is the contents of the file after the change:

glibc3

  1. Issue the command “mkdir -p ~/spo600/lab4/build/glibc” to create the build directory.
  2. Issue the command “cd ~/spo600/lab4/build/glibc” to move to the build directory.
  3. Issue the command “~/spo600/lab4/src/glibc/configure –prefix=/home/cslam4/spo600/lab4/build/glibc” to run the “configure” script to configure the package and create the “Makefile” file for my system. The “–prefix” option tells “configure” where to install the GNU C Library:

glibc1

  1. After “configure” runs successfully, we use the command “make” to compile glibc:

glibc2

  1. After compilation is complete, I create the program “randnum.c” to test the newly built glibc:
#include <stdio.h>
#include <stdlib.h>

int main () {
        // Print 10 random numbers
        int i;
        for( i = 0 ; i < 10 ; i++ ) {
                printf("Number %d: %d\n", i + 1, rand());
        }
        return 0;
}

This program will display 10 random numbers. I can use the “testrun.sh” script in the build directory to run my program using the newly built glibc. To do that, I issue the command “./testrun.sh ~/spo600/lab4/randnum” from the build directory. We see that we get the number 5 instead of a random number, which means that I am using the newly built glibc:

glibc4

When I run my program using the system glibc using the command “./randnum”, I get 10 random numbers:

glibc5

It was great to see the two different results!

Compared to grep, compiling and testing glibc is more complicated and involves more steps. For glibc, we need a directory to store the source code and a different directory to compile glibc. We also need to install and update some tools in order to compile glibc and run some test scripts. For testing, we need to change the source code for a function to alter the function’s behaviour in order to test our version of glibc. It took me a bit of time to find out where the source file for the function is located.

Override and multiarch

There may be more than one implementation for each function in a shared library due to the presence of different architectures and each version optimized for a specific architecture. We can override a version of a function with another such as our own version. The “ldd” command is used to show the shared library dependencies for a program. When we execute a program, the dynamic linker is being invoked. The dynamic linker will search for these shared libraries based on specific configuration files and environment variables, load these libraries into memory and link everything together before executing the program. The dynamic linker allows us to load other libraries and override functions in shared libraries using the LD_PRELOAD environment variable. To override a function, we need to build a shared library with our version of the function. We use the “gcc” and “ld” commands to build the source file into a shared library. Next, we set the LD_PRELOAD environment variable to the path of our shared library to load our shared library when calling the function. It is important to note that this override method does not work with system calls such as printf and scanf. There may be other methods to override functions but that will require more research.

Currently, many software packages are built for a specific architecture. We can only install one version of a package on one system. Multiarch allows more than one platform-specific version of a package to be installed on the same system. Cross-architecture dependencies can be installed and cross compilation is possible. We can install a package built for another architecture on our system. With multiarch, we can install a library of different architectures on a single system. In the glibc source directory, the “sysdeps” directory contains a number of architecture-specific directories, each of which contains functions written in C or assembler for the specific architecture. When we run the “configure” script before compiling glibc, it searches for these architecture-specific directories for architecture-specific functions to use based on the operating system, the manufacturer’s name, and the CPU type. In the architecture-specific directory, we also see the “multiarch” directory and it contains more implementations of functions written in C or assembler. The “multiarch” directory also contains some IFUNC files. Indirect function support (IFUNC) is a feature that allows an implementation of a function to be selected at runtime using a resolver function. When we call a function, the dynamic loader runs a resolver function to select the best implementation of that function to be used by the application.

Design a site like this with WordPress.com
Get started