Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Function multiversion resolver function implementation #72

Open
BeMg opened this issue Apr 9, 2024 · 2 comments
Open

[RFC] Function multiversion resolver function implementation #72

BeMg opened this issue Apr 9, 2024 · 2 comments

Comments

@BeMg
Copy link
Contributor

BeMg commented Apr 9, 2024

When generating the resolver function for function multiversioning, a mechanism is needed to retrieve the environment information.

To achieve this goal, several steps need to be taken:

  1. Collect the required extensions for a particular function.
  2. Transform these required extensions into a platform-dependent form.
  3. Query whether the environment fulfills these requirements during runtime.

Step 1 is handled by the compiler, while step 3 must follow the necessary steps from the platform during runtime.

This RFC aims to propose how the compiler and runtime function can tackle problem 2.

Here is a example

__attribute__((target_clones("default", "arch=rv64gcv"))) int bar() {
    return 1;
}

In this example, there are two versions of function bar. One for default, another for "rv64gcv".

If environment fullfills the requirement, then bar could use the version arch=rv64gcv. Otherwise, It invokes with default version.

This process be controlled by the ifunc resolver function.

ptr bar.resolver() {
   if (isFullFill(...))
      return "bar.arch=rv64gcv";
   return bar.default;
}

The isFullFill should available during the program runtime.

The version arch=rv64gcv require

i, m, a, f, d, c, v, zicsr, zifencei, zve32f, zve32x, zve64d, zve64f, zve64x, zvl128b, zvl32b, zvl64b,

The problem 2 is about where to maintain the relationship between extension names and platform-dependent probe forms.

Here are three possible approach to achieve goal.

  1. Encode all required extensions into a string format, then let the platform implement its own probe approach based on the string inside the runtime function. This approach maintains the relationship between extension names and platform-dependent probe forms inside the runtime function.
ptr bar.resolver() {
   if (isFullFill("i;m;a;f;d;c;v;zicsr;zifencei;zve32f;zve32x;zve64d;zve64f;zve64x;zvl128b;zvl32b;zvl64b"))
      return bar.arch=rv64gcv;
   return bar.default;
}

bool isFullFill(char *ReqExts) {
    if (isLinux())
       return doLinuxRISCVExtensionProbe(ReqExts);
    if (isFreeBSD())
       return doFreeBSDRISCVExtensionProbe(ReqExts);
    // Other platform
    ....
    return false;
}
  • Pros
    • Human readable
    • Relatively high portability
    • Provides a uniform interface for all platforms
  • Cons
    • Requires extra effort for string processing in the runtime function.
  1. Encode all required extensions into a compiler-defined key, then let the platform implement its own probe approach inside the runtime. This approach maintains the relationship between the compiler-defined key for extensions and the platform-dependent probe form inside the runtime function.
// Assume compiler define
// i -> 1
// m -> 2
...

ptr bar.resolver() {
   if (isFullFill([1, 2, 3, 8, ...], length))
      return bar.arch=rv64gcv;
   return bar.default;
}

bool isFullFill(int *ReqExts, length) {
    if (isLinux())
       return doLinuxRISCVExtensionProbe(ReqExts, length);
    if (isFreeBSD())
       return doFreeBSDRISCVExtensionProbe(ReqExts, length);
    // Other platform
    ....
    return false;
}
  • Pros
    • Doesn't require string processing during runtime
    • Provides a uniform interface for all platforms
  • Cons
    • Requires maintaining the relationship between the compiler-defined key for extensions and the concrete extension names inside runtime function.
  1. Define a different runtime function for each platform and construct any necessary information during compilation time if necessary for the platform. This approach maintains the relationship between extension names and platform-dependent probe forms inside the compiler.
// If compiler compile for linux, then use bar.resolver.linux
ptr bar.resolver.linux() {
   if (isFullFillLinux(LinuxProbeObject))
      return bar.arch=rv64gcv;
   return bar.default;
}

ptr bar.resolver.freebsd() {
   if (isFullFillFreeBSD(FreeBSDProbeObject))
      return bar.arch=rv64gcv;
   return bar.default;
}

// Other platform bar.resolver
...

bool isFullFillLinux(LinuxProbeObject Obj) {
   return doLinuxProbe(Obj);
}

bool isFullFillFreeBSD(FreeBSDProbeObject Obj) {
   return doFreeBSDProbe(Obj);
}

// Other platform isFullFill
...

  • Pros
    • Relatively simple implementation for the runtime function
  • Cons
    • Does not provide a uniform interface for all platforms
@lenary
Copy link
Contributor

lenary commented Jul 16, 2024

You've gone down the route of 2, which makes sense to me.

One thing you've glossed over is part of the third stage: each function version is for a specific set of features, but nothing requires that each set of features is a strict super-/sub-set of those for other versions of the same function. Does it matter that there's not a defined order for choosing between the two sets?

For example:

  • The default version of f just has I, no extensions.
  • One version of x is for when you have I and the C extension.
  • One version of x is for when you have I and the F extension.
  • The CPU you're running on implements I, C and F - which version of x do you choose?

Maybe it's fine to leave it undefined? Maybe it's fine to be reasonably nondeterministic? I'm not sure. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants