riscv-non-isa / riscv-c-api-doc

Documentation of the RISC-V C API
https://jira.riscv.org/browse/RVG-4
Creative Commons Attribution 4.0 International
66 stars 38 forks source link

[RFC] Function multiversion resolver function implementation #72

Open BeMg opened 3 months ago

BeMg commented 3 months ago

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;
}
  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;
}
  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
...
BeMg commented 3 months ago

This RFC relate to

lenary commented 5 days ago

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:

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