Week 7B - IFUNC, FMV, & AFMV !?

Hamza TeliHamza Teli
6 min read

I know what you are thinking… Whats with all the acronyms. (That’s the same response I had when joining this lecture by Professor Chris Tyler). But do not fret, I will try my best to explain what I learned and also convey it in an easy to digest manner.

Before I begin, understand the following that “Different implementations of the same architecture may have very different capabilities” - Professor Chris Tyler. What this means is that you can have an x86 architecture chip that can be used for simple tasks and can cost around a few dollars, likewise, you can have a chip thats sole purpose is for advanced data analytics that can cost thousands. Same architecture but different capabilities = different costs.

This means that when we are writing code, how do we ensure that it can target a whole range of systems? This is a problem in the industry. Solutions for this problem include

  • Compile-time code selection

  • Run-time codepath selection

What is Compile-Time Code Selection?

Compile-time code selection is when you create different binaries for different hardware. So what this does, is it ensures theres no runtime overhead and can improve performance. But the issue with this is that developers need to manage multiple versions of binary for different hardware. Theres no single binary encompassing everything.

What is Run-Time codepath selection?

Run-time code path selection chooses the code path based on hardwares capabilities. This allows us to have one big binary for multiple hardware. The tradeoff with this is its a bit slower.

Based off the two selections, we seen, its evident that we want an ideal runtime codepath selection which involves taking advantage of the hardwares capabilities with minimal overhead and changes to code. We have three solutions ro runtime codepath selection:

  • GNU iFunc

  • Function Multiversioning

  • Automatic Function Multiversioning

What is GNU iFunc (Indirect Functions)?

GNU iFunc will perform indirect function calls which means it allows a program to choose between multiple versions of the same function. For example, you can have one function for a more complex CPU and one for a basic one. But how does it do this? The program sets up a pointer at runtime to point to one of the available implementations. It does this by using a resolver function that decides which implementation to use.

Whats a resolver function?

A resolver function is a function that runs when your program starts and it basically determines the systems capabilities. By doing this, it points to the best implementation possible of a function.

For each function that we have multiple implementations of we have a function prototype and inside the prototype, one argument is the resolver. Look at the example below:

  • returnType *name (params) attribute((ifunc(“resolver”)));

So the resolver will return the function thats best based on the systems capabilities.

How does the resolver figure out the details of the current hardware?

There are 2 ways:

  • Use machine specific instructions: There are specific read only registers that tell us the details of the machines capabilities. Using this data we can determine the capabilities. An example is using CPUINFO

  • Using the OS, getauxval() function: this function gives us information about the system. Its a lot easier to use than the method above as it doesn’t require as much coding and knowing the system.

    • We can grab values from the auxillary vector and use the && operation to get bits like below.

      •   unsigned long hwcaps = getauxval(AT_HWCAP)
          SVE = (hwcaps && HWCAP_SVE)
        
          unsigned long hwcaps2 = getauxvalAT_HWCAP2)
          SVE2 = (hwcaps2 && HWCAP2_SVE2)
        

        Now we can use SVE and SVE2 variables to determine the systems capabilities and determine which function to use, i.e. if the system is SVE, return functionA else return functionB.

Advantages & Disadvantages to iFunc

Advantages include:

  • We can go beyond just detecting architectural features like detect how much free RAM there is and this allows us to fine tune the algorithm based on available RAM. Or we can check how many cores we got and see if we can use a multi threaded approach.

Disadvantages:

  • Requires source code changes: means we have to change the source code

  • A resolver function is necessary

  • You need multiple versions of a function

  • We have to keep modifying our code as the machine becomes more complex

What is Function Multiversioning (FMV)?

Function multiversioning is another approach and is built on top of iFunc and is rarely used. With FMV we do have multiple versions of a function, but the resolver function is automatically generated by the compiler (amazing) which means we don’t have to write any resolvers.

There are two versions of FMV:

  • Manual Function Multiversioning: You have 2 definitions of a function with the same name but the only difference is that you have a different target_version. Look at the example below

    •   __attribute__(( target_version("abc")) ) func() {};
        __attribute__(( target_version("def")) ) func() {};
      
  • Cloning Function Multiversioning: Instead of writing two different versions, we can write one version and tell the system to clone it into multiple versions. This is done by using target_clone instead of target_version. And inside target_clone, we include the different versions. These versions have same source code but are compiled differently. Look at the code below:

    •   __attribute__(( target_clone("abc", "def") )) func() {};
      

Advantages & Disadvantages of FMV

Advantages:

  • The compiler automatically creates the resolver function.

Disadvantages:

  • It does require a small source code change

What is Automatic Function Multiversioning (AFMV)?

This does not exist! In this course we are looking at a way to do this. But what exactly are we trying to achieve here? Essentially, AFMV would automatically create multiple versions of a function where useful.

The ideal characteristics include:

  • Zero code changes

  • Versions are specified in the build system: where we can use gcc with a flag option

  • Compiler clones every function: after cloning all functions, it compares them after optimization. It identifies the ones that are not the same and gets rid of the rest.

Advantages of AFMV

  • No source code changes

  • No resolver function required

  • You do need multi versions of a function

  • Maintenance going forward is minimal, we don’t have to change AFMV code. Just change the command line flags in gcc.

AFMV sounds quite exciting right? For this course, we won’t necessarily create the AFMV entirely but we will do a specific part of it and that is the function pruning aspect. In simple terms, this means identifying all the function clones that are same and getting rid of the duplicates. We can do this by maybe building a signature of a function by analyzing it, and using this signature we can compare it with the signature of other functions. This hash/signature creation has to be done in a way to ensure we factor in random variable names, we want to exclude these factors and just focus on whether the logic is different.

References

0
Subscribe to my newsletter

Read articles from Hamza Teli directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hamza Teli
Hamza Teli