top of page
Writer's picturearman valaee

Software Portability Project - Stage 3.1 Final Implementation + Added Features

In this blog, I will focus on the modifications and improvements I made to stage 2 of this project.

To learn about the basics of the program, its purpose, and the initial implementation, visit the following links:

Let's start with the additional features added to this program!



In the previous stage, I made a tool that gets a main.c file, and a function.c file which includes one function.

Using the tool we make a new function file called function_altered.c which includes 3 different implementations of our function. The implementations are planned to use different auto-vectorization mechanisms based on the user's machine.

This tool is planned to work only on machines with Aarch64 structure.

The 3 auto-vectorization mechanisms include:

  • ASIMD

  • SVE

  • SVE2

This tool also requires the funcion.h file to run properly in stage 2, in which we used the makeheaders feature to create the file and find the prototype of the function. In stage 2 we assumed that the header file is non-existent and is already included in the main file. This is one of the limitations that I worked on and improved in stage 3.



Header Files Generator


One of the limitations of stage 2 was not being able to accept and process more than 1 function.c file. Also function.h file should've been created beforehand, assuming it is non-existent.


In stage 3, the user does not need to enter the makeheaders command manually. It is already implemented in our tool and it will create the header file using the python os module.

This is the part of the code that handles it:

# Find function name and store as string - remove .c
    funcFile = num.split(".")[0] 
# Add .h to the function name                                       
    funcHeader = funcFile + ".h"                                        

# Create the header file if non-existant        
    if (os.path.isfile(funcHeader) == False):                           
        makeFuncHeader = "makeheaders " + num
        os.system(makeFuncHeader)

One interesting about this approach is that it won't overwrite any predefined header files. if a file, with the same name as our function file with the .h extension, exist in the same directory, the program will just ignore these lines of code.

Note: Both files (.c and .h) should have the same name and just different extensions, otherwise this wouldn't work correctly.


The next important detail about the related improvement is that it will also add the #ifndef FUNCTION_H, #define FUNCTION_H, and #endif lines to the header file.

These are the lines of the code that are responsible for adding these lines:

    fheader = open(funcHeader, "r")
    proto = fheader.read()
    fheader.close()
    
    headerDefine = "#ifndef " + funcFile.upper() + "_H\n" + "#define " +     
            funcFile.upper() + "_H\n\n" + proto + "\n\n#endif"
    fheader = open(funcHeader, "w")
    fheader.write(headerDefine)
    fheader.close()

For example, a header file create for the file called function.c looks like this:

#ifndef FUNCTION_H
#define FUNCTION_H

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


#endif

Everything included in this file is auto-generated based on the function name and the prototype.



main.c File modification


We can assume that the generated header files are already included in the main.c file, but in stage 3, I do not want to make any assumptions!

If we don't make any assumptions about the main.c file, it is possible that it does not include our generated header files. So we add them!

Modifying the main.c file might not be our best option, therefore this program creates a new file which is called main_test.c.

It is a copy of the original main.c file + the auto-generated header files inclusion.

main_test.c may look like this:

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

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

Also here is the code that I wrote to create a new file, write the original main content to it, and add the included lines on top:

    -------------------------------------- In a loop to get functions
    newMain = open("main_test.c", "w")
    mainIncludes = mainIncludes + '#include "' + funcHeader + '"\n'
    newMain.write(mainIncludes)
    newMain.close()
    runScript += function_altered[idx] + " "
    idx += 1
    -------------------------------------- End of loop
 
newMain = open("main_test.c", "w")
newMain.write(mainIncludes + seperator + mainContent)
newMain.close()


Multiple Functions


One of our limitations in stage 2 is not being able to input more than one function file, which is a noticeable limitation, since in the real world and more importantly large-scale projects we require more than one function file.

In this stage, we allow the user to input as many as function files as needed.

Also, they do not need to input the header files anymore!


For this purpose, I used a loop to go through the received arguments, which are the function files, and make the required modifications one by one.

The required modifications are explained in Stage 2.1 Initial Implementation.



Error Message Update


If you remember from the previous stage, if the user enters any number of arguments except 2, would receive an error message that says: "Invalid Arguments"


This was not very clear or helpful to the user, as they might not know how to use and run this tool.

In stage 3, users can enter as many arguments as they need, but if they run the program with no argument the updated error message will appear.

This error message contains everything you need to know about this program.

From how to run it, how to use useful features, and how the program works, to the source file link in GitHub and even this blog!

This is the updated error message:


------------------------------Invalid Arguments---------------------------

This Program Requires at least 1 function to operate correctly

Add as many function files as needed after ./ifuncCreator

Example: ./ifuncCreator function.c function1.c function2.c ...

Note: As seen in the example above, function header files should not be included in the run command


Function header files may or may not be existant in the same directory

Existant header files will not be modified, but they should have the same name as the function file with .h extension

Non-existant header files will be automatically generated using makeheaders feature

generated header file will have the same name as the function file with .h extension and it will include the function prototype

It is necessary for main.c and resolver.txt files to be existant in the same directory

main.c file will not be modified, instead main_test.c will be created and used with the gcc compiler

For more information please visit:

     https://github.com/Awrmani/Aarch64-iFunc-Vectorization

     https://armanvalaee.wixsite.com/arman-valaee-asr/blog

------------------------------Enjoy Using This Tool-----------------------


Built-In Compilation


Normally, users need to compile this program, then use the execution files to run their main.c program regarding their machine, which this program will decide about.

In this stage, I added a feature to also compile their code after creating the required files using this tool. Built-In!

Using this feature user does not need to compile their files using the following command afterward!

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

Also, since this program can handle an unlimited number of function files, if a huge number of files exist, it will make it even harder.

But the built-in compilation feature will handle that using every input file!

How to use it?

All you need to do is to write "compile" after the actual program run command.

For example:

./ifuncCreator function.c function1.c function2.c ... compile

You might have noticed that you no longer need to write "python" to run this command! Also, the .py extension has been removed from our file name.



Makefile


Finally, a makefile has been added to make the program running process much easier and more understandable.

Unfortunately, I did not have much time to make it perfect, so we can only run the program using one argument file, which is called function.c.

To be more exact, this Makefile has been created to make the process of testing easier and faster.

To use the complete features you can still use the following code:

./ifuncCreator function.c function1.c function2.c ... compile

, or this one to not compile the actual program and main.c

./ifuncCreator function.c function1.c function2.c ... 

If the user chooses to use this code, a message that explains the next steps and some extra tips will appear on the screen which may help them during their process! Here is the message:


Use the following command tu compile your program:

     gcc -g -o3 main_test.c function_altered.c function1_altered.c ... -o ifuncMain


Note: If you plan on using your original main function instead of the main_test.c,

(which is a copy of the original main.c + header files added on top), use main.c instead of main_test.c in the above commands.


To use the makefile with one function (function.c):

make ifuncTool

For no compilation:

make ifuncTool_noCompile

There are also features available to test the program with SIMD, SVE, and SVE2 using the bree.jpg file as the input, but I will get to that in the next blog which is dedicated to the testing process.


Here is the content of the makefile:

ifuncTool: function.c
    ./ifuncCreator function.c compile

ifuncTool_noCompile: function.c
    ./ifuncCreator function.c

testSIMD: 
    ./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

testSVE2:
    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

clean:
    rm tests/output/bree??.jpg

To see the testing process and results, please view the next blog, Stage 3.2.


Also here is the complete code of our tool, ifuncCreator, even though you can find it in my GitHub Repository:


#!/usr/bin/python3 import os import sys if len(sys.argv) < 2: print("------------------------------Invalid Arguments------------------------------\n") print("This Program Requires at least 1 function to operate correctly\n") print("Add as many function files as needed after ./ifuncCreator\n") print("Example: ./ifuncCreator function.c function1.c function2.c ...\n") print("Note: As seen in the example above, function header files should not be included in the run command\n\n") print("Function header files may or may not be existant in the same directory\n") print("Existant header files will not be modified, but they should have the same name as the function file with .h extension\n") print("Non-existant header files will be automatically generated using makeheaders feature\n") print("generated header file will have the same name as the function file with .h extension and it will include the function prototype\n") print("It is necessary for main.c and resolver.txt files to be existant in the same directory\n") print("main.c file will not be modified, instead main_test.c will be created and used with the gcc compiler\n") print("For more information please visit:\n") print(" https://github.com/Awrmani/Aarch64-iFunc-Vectorization\n") print(" https://armanvalaee.wixsite.com/arman-valaee-asr/blog\n") print("------------------------------Enjoy Using This Tool------------------------------") sys.exit(1) main = open("main.c", "r") mainContent = main.read() main.close() mainIncludes = "// Auto Generated Header File Includes\n" runScript = "gcc -g -O3 main_test.c " seperator = "\n\n// -----------------------------------------------------------------\n\n" function_altered = [] idx = -1 for num in sys.argv: if (idx == -1): # Ignore first input idx += 1 continue if (num == "compile"): break funcFile = num.split(".")[0] # Find function name and store as string - remove .c funcHeader = funcFile + ".h" # Add .h to the function name if (os.path.isfile(funcHeader) == False): # Create the header file if non-existant makeFuncHeader = "makeheaders " + num os.system(makeFuncHeader) fheader = open(funcHeader, "r") proto = fheader.read() fheader.close() if (proto.find("#ifndef") == -1): headerDefine = "#ifndef " + funcFile.upper() + "_H\n" + "#define " + funcFile.upper() + "_H\n\n" + proto + "\n\n#endif" fheader = open(funcHeader, "w") fheader.write(headerDefine) fheader.close() proto = str(proto) start = proto.find("*/") end = proto.find(";") proto = proto[start + 3 : end + 1] # Original function prototype start = proto.find(" ") end = proto.find("(") funcName = proto[start + 1 : end] # Original function name # Adjusting function names based on different implementations funcNameSIMD = funcName + "_SIMD" funcNameSVE = funcName + "_SVE" funcNameSVE2 = funcName + "_SVE2" ffunction = open(num, "r") funcOrigin = ffunction.read() ffunction.close() funcOrigin = str(funcOrigin) # Original function.c file content includeSysAux = "#include <sys/auxv.h>\n\n" iFuncProto = '__attribute__ (( ifunc("resolve_' + funcName + '") )) ' + proto # Function prototype with the added assembly ifunc attribute funcOriginSIMD = funcOrigin.replace(funcName, funcNameSIMD) pragmaSIMD = '\n\n#pragma GCC target "arch=armv8-a"\n\n' # Pragma Used for SIMD function funcOriginSVE = funcOrigin.replace(funcName, funcNameSVE) pragmaSVE = '\n\n#pragma GCC target "arch=armv8-a+sve"\n\n' # Pragma Used for SVE function funcOriginSVE2 = funcOrigin.replace(funcName, funcNameSVE2) pragmaSVE2 = '\n\n#pragma GCC target "arch=armv8-a+sve2"\n\n' # Pragma Used for SVE2 function fresolver = open("resolver.txt", "r") # Using resolver.txt as iFunc resolver function resolverFunc = fresolver.read() fresolver.close() resolverFunc = str(resolverFunc) # Altering iFunc function values based on the function names in function.c file resolverFunc = resolverFunc.replace("<<function_name>>", funcName) resolverFunc = resolverFunc.replace("<<function_sve2>>", funcNameSVE2) resolverFunc = resolverFunc.replace("<<function_sve>>", funcNameSVE) resolverFunc = resolverFunc.replace("<<function_simd>>", funcNameSIMD) function_altered.append(funcFile + "_altered.c") # Creating final function_altered.c filed with 3 implementations of function.c, including their pragmas and the ifunc resolver ffunctionAltered = open(function_altered[idx], "w") ffunctionAltered.write(includeSysAux + iFuncProto + pragmaSIMD + funcOriginSIMD + seperator + pragmaSVE + funcOriginSVE + seperator + pragmaSVE2 + funcOriginSVE2 + seperator + pragmaSIMD + seperator + resolverFunc) ffunctionAltered.close() newMain = open("main_test.c", "w") mainIncludes = mainIncludes + '#include "' + funcHeader + '"\n' newMain.write(mainIncludes) newMain.close() runScript += function_altered[idx] + " " idx += 1 newMain = open("main_test.c", "w") newMain.write(mainIncludes + seperator + mainContent) newMain.close() runScript += "-o ifuncMain" if (sys.argv[-1] == "compile"): print("Using command: ") print(runScript) os.system(runScript) else: print("Altered function(s) using iFunc Resolver have been created\n") print("Use the following command tu compile your program:\n") print(" gcc -g -o3 main_test.c function_altered.c function1_altered.c ... -o ifuncMain\n\n") print("Note: If you plan on using your original main function instead of the main_test.c,\n") print("(which is a copy of the original main.c + header files added on top), use main.c instead of main_test.c in the above commands.")


13 views0 comments

Comments


bottom of page