top of page
Writer's picturearman valaee

Software Portability Project - Stage 3.2 Testing

In this blog we will use the run commands explained in my previous blog to test the functionality and the feature of this tool!




To begin with, let's take a look at my repository for the ifuncCreator tool!

To make the testing process easier, it is recommended to clone the repository in a local directory using the "git clone" command.


There are 2 function files provided in this repository, in addition to the main.c file.

These files are mainly for testing purposes.

Also as you can see there is a function.h file available, but there are no header files available for function1.c. I did this intentionally to test the auto header file generator feature of this tool.


After running the following command which was explained in the previous blog, the function.h file will remain unchanged, but a new file called function1.h will be created which is function1.c header file and it includes the function prototype.



How to Run


Running this program is fairly easy. There are two ways to run it and I recommend you try both!

As I explained in stage 3.1, a Makefile is provided to make this task easier so simply enter the following command to run the program using 1 function file which is function.c.

make ifuncTool

This command will execute the ifuncCreator with function.c as the only input.

This task will create the function.h (if non-existent, which in this case we already have in our directory so it won't do anything with function.h file), function_altered.c, and main_test.c

This make command requires function.c as the only input and it will compile the main.c after creating the additional files.

The following command is already included in the ifuncCreator so you do not need to compile it again.

gcc -g -O3 main.c function_altered.c -o ifuncMain

Therefore the execution file, ifuncMain will also be created!


As you can see in the screenshot above, the ifuncMain file + the function_altered.c file has been created!


Another approach to using the Makefile is the following command:

make ifuncTool_noCompile

This will do the same thing without compiling the main.c, but it will show a message that informs the user about the next steps!



Testing ./ifuncMain with some inputs!


Makefile comes in very handy when it comes to testing our main.c execution file using ifunc feature!

We can either use the commands:

    ./ifuncMain tests/input/bree.jpg 1.0 1.0 1.0 tests/output/bree1a.jpg
    ./ifuncMain tests/input/bree.jpg 0.5 0.5 0.5 tests/output/bree2a.jpg
    ./ifuncMain tests/input/bree.jpg 2.0 2.0 2.0 tests/output/bree3a.jpg

These commands will take the bree.jpg input file, modify it based on the chosen implemented function. The function will be decided using the iFunc resolver function which we added to the function_altered.c.

The output file which is the result of the program will be saved in tests/output.

An easier way to test using these input files is prepared in the Makefile.

You can simply write:

make testSIMD

And it will automatically run the above commands!

In the screenshot above, you can see the 3 output files has been created.

Also in the highlighted lines, you can see that the SIMD implementation of the function has been used.

This is because the aarch64 machine in Israel does not support SVE or SVE2.

But there is a way to test it using an SVE2 simulation. By using qemu-aarch64 command!

Before getting to that, let's view our output files!




As you can see the adjust_channels function is working perfectly!

Now, let's test it with the qemu-aarch64 simulator to see if it uses a different implementation.

For this task you can use the following commands:

    qemu-aarch64 ./ifuncMain tests/input/bree.jpg 1.0 1.0 1.0 tests/output/bree1b.jpg
    qemu-aarch64 ./ifuncMain tests/input/bree.jpg 0.5 0.5 0.5 tests/output/bree2b.jpg
    qemu-aarch64 ./ifuncMain tests/input/bree.jpg 2.0 2.0 2.0 tests/output/bree3b.jpg

Or simply use:

make testSVE2

Now let's see the result on a machine that is capable of SVE2 auto-vectorization:

3 new output files have been created and as you can see in the highlighted lines, our function is using the SVE2 implementation since we used the qemu-aarch64 simulator!



Multiple Functions + Header file generator Test


In the previous blog, I mentioned that I added some features, including the ability to take multiple functions at the same time, and create a header file for the function files that do not have one!

For this test, I will be using function.c and adjuster.c!

function.c comes with the header file, so the program should not make any changes to it. But the adjuster.c does not have a header file therefore this program should create an adjuster.h file.

main_test.c file will be created with the header file inclusions.

We also expect function_altered.c and adjuster_altered.c files to be created as usual!


There is no Makefile command prepared for this test so we have to manually write it. Here is the command:

./ifuncCreator function.c adjuster.c compile

Note: We can also remove the "compile" from our command, but we have to manually compile the main.c or main_test.c file afterward.


Here is the result of this command:

The lines that are highlighted are the newly created files.

  • adjuster_altered.c

  • adjuster.h

  • function_altered.c

  • main_test.c

  • ifuncMain


Lets take a look at some of these files:


adjuster_altered.c

#include <sys/auxv.h>

__attribute__ (( ifunc("resolve_channel_adjuster") )) void channel_adjuster(unsigned char *image,int x_size,int y_size,float red_factor,float green_factor,float blue_factor);

#pragma GCC target "arch=armv8-a"

/*

        channel_adjuster_SIMD :: adjust red/green/blue colour channels in an image
        
        The function returns an adjusted image in the original location.
        4
        Copyright (C)2022 Seneca College of Applied Arts and Technology
        Written by Chris Tyler
        Distributed under the terms of the GNU GPL v2
        
*/

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

// ----------------------------------------------------------------- Naive implementation in C

#include <sys/param.h>

void channel_adjuster_SIMD(unsigned char *image, int x_size, int y_size, 
        float red_factor, float green_factor, float blue_factor) {

        printf("Using channel_adjuster_SIMD() implementation #1 - Naive (autovectorizable)\n");
        
/*

        The image is stored in memory as pixels of 3 bytes, representing red/green/blue values.
        Each of these values is multiplied by the corresponding adjustment factor, with 
        saturation, and then stored back to the original memory location.
        
        This simple implementation causes int to float to int conversions.
        
*/

        for (int i = 0; i < x_size * y_size * 3; i += 3) {
                image[i]   = MIN((float)image[i]   * red_factor,   255);
                image[i+1] = MIN((float)image[i+1] * blue_factor,  255);
                image[i+2] = MIN((float)image[i+2] * green_factor, 255);
        }
}



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



#pragma GCC target "arch=armv8-a+sve"

/*

        channel_adjuster_SVE :: adjust red/green/blue colour channels in an image
        
        The function returns an adjusted image in the original location.
        4
        Copyright (C)2022 Seneca College of Applied Arts and Technology
        Written by Chris Tyler
        Distributed under the terms of the GNU GPL v2
        
*/

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

// ----------------------------------------------------------------- Naive implementation in C

#include <sys/param.h>

void channel_adjuster_SVE(unsigned char *image, int x_size, int y_size, 
        float red_factor, float green_factor, float blue_factor) {

        printf("Using channel_adjuster_SVE() implementation #1 - Naive (autovectorizable)\n");
        
/*

        The image is stored in memory as pixels of 3 bytes, representing red/green/blue values.
        Each of these values is multiplied by the corresponding adjustment factor, with 
        saturation, and then stored back to the original memory location.
        
        This simple implementation causes int to float to int conversions.
        
*/

        for (int i = 0; i < x_size * y_size * 3; i += 3) {
                image[i]   = MIN((float)image[i]   * red_factor,   255);
                image[i+1] = MIN((float)image[i+1] * blue_factor,  255);
                image[i+2] = MIN((float)image[i+2] * green_factor, 255);
        }
}



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



#pragma GCC target "arch=armv8-a+sve2"

/*

        channel_adjuster_SVE2 :: adjust red/green/blue colour channels in an image
        
        The function returns an adjusted image in the original location.
        4
        Copyright (C)2022 Seneca College of Applied Arts and Technology
        Written by Chris Tyler
        Distributed under the terms of the GNU GPL v2
        
*/

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

// ----------------------------------------------------------------- Naive implementation in C

#include <sys/param.h>

void channel_adjuster_SVE2(unsigned char *image, int x_size, int y_size, 
        float red_factor, float green_factor, float blue_factor) {

        printf("Using channel_adjuster_SVE2() implementation #1 - Naive (autovectorizable)\n");
        
/*

        The image is stored in memory as pixels of 3 bytes, representing red/green/blue values.
        Each of these values is multiplied by the corresponding adjustment factor, with 
        saturation, and then stored back to the original memory location.
        
        This simple implementation causes int to float to int conversions.
        
*/

        for (int i = 0; i < x_size * y_size * 3; i += 3) {
                image[i]   = MIN((float)image[i]   * red_factor,   255);
                image[i+1] = MIN((float)image[i+1] * blue_factor,  255);
                image[i+2] = MIN((float)image[i+2] * green_factor, 255);
        }
}



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



#pragma GCC target "arch=armv8-a"



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

// Resolver function - this function picks which of the
// implementations will be executed when foo() is called
//
// The resolver function is only run once, the first time
// that foo() is called.
//
static void (*resolve_channel_adjuster(void)) {
        // Each of these two variables is populated with
        // a bitfield indicating specific hardware 
        // capabilities. hwcaps includes a bit for SVE,
        // and hwcaps2 includes a bit for SVE2
        //
        long hwcaps  = getauxval(AT_HWCAP);
        long hwcaps2 = getauxval(AT_HWCAP2);

        printf("\n### Resolver function - selecting the implementation to use for channel_adjuster()\n");
        if (hwcaps2 & HWCAP2_SVE2) {
                return channel_adjuster_SVE2;
        } else if (hwcaps & HWCAP_SVE) {
                return channel_adjuster_SVE;
        } else {
                return channel_adjuster_SIMD;
        }
};

adjuster.h

#ifndef ADJUSTER_H
#define ADJUSTER_H

/* This file was automatically generated.  Do not edit! */
void channel_adjuster(unsigned char *image,int x_size,int y_size,float red_factor,float green_factor,float blue_factor);


#endif


main_test.c

// Auto Generated Header File Includes
#include "function.h"
#include "adjuster.h"


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

/*
  image-adjust
  
  (C)2022 Seneca College of Applied Arts and Technology.
  Written by Chris Tyler. Licensed under the terms of the GPL verion 2.
  
*/

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h>

// adjust_channels is where all the real action is
// this file is just scaffolding!
//#include "adjust_channels.h"

// Using the STBI image reader/writer
// See https://github.com/nothings/stb
#define STBI_NO_LINEAR
#define STBI_NO_HDR
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>

int main(int argc, char *argv[]) {

    // ==================== Check arg count
    if (argc != 6) {
        dprintf(2, "\nUsage: %s input.jpg red green blue output.jpg\nWhere red/green/blue are in the range 0.0-2.0\n", argv[0]);
        return 1;
    }

    // ==================== Load the image file (arg 1)
    int x, y, n;
    unsigned char *image = stbi_load(argv[1], 
        &x, &y, &n, 3);

    if (image == NULL) {
        dprintf(2, "Invalid argument or input image file did not load.\n");
        dprintf(2, "\nUsage: %s input.jpg red green blue output.jpg\nWhere red/green/blue are in the range 0.0-2.0\n", argv[0]);
        return 2;
    }
    printf("File '%s' loaded: %dx%d pixels, %d bytes per pixel.\n", argv[1], x, y, n);


    
    // ==================== Adjust the channels
    
    // Get arguments 2, 3, and 4; each should be a number in the range 0.0 .. 2.0
    // Yes this is ugly and should be improved, this is a quick & dirty test program :-)
    float redarg   = MIN(2, MAX(0, strtof(argv[2],NULL)));
    float greenarg = MIN(2, MAX(0, strtof(argv[3],NULL)));
    float bluearg  = MIN(2, MAX(0, strtof(argv[4],NULL)));
    
    printf("Adjustments:\tred: %8.6f   green: %8.6f   blue: %8.6f\n", redarg, greenarg, bluearg);
    
    adjust_channels(image, x, y, redarg, greenarg, bluearg);

    // ==================== Save the resulting file (jpg) (arg 5)
    stbi_write_jpg(argv[5], x, y, n, image, 90);
}

In the screenshot above you can see the main_test.c compilation command is also printed and includes the main_test.c plus the altered files for all the functions so we can tell that the built-in compilation feature is working well!


In this program, we could tell which implementation of the function has been used since we also altered the print message in each function to show us which implementation is it.

But if this was not the case, we could use the command "objdump" to check the assembly code and see how is it being compiled.


How can this help us?

In the object dump, we can check the registers that were involved with compiling.

With the non-sve mechanism, there should be no occurrence of the z register, but after we used the qemu-aarch64 simulator, you should be able to see the usage of the z register in the object dump.


Thats it for testing this tool and its added functionality and features! This is the end of this project!

Thank you for reading and I hope this was helpful and enjoyable to you, as it was to me to create this beatiful tool!




12 views0 comments

Comments


bottom of page