Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-88504

Improve Deferred usability

    • Type: Icon: Task Task
    • Resolution: Unresolved
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Query Optimization
    • QO 2024-04-01, QO 2024-04-15, QO 2024-04-29, QO 2024-05-13, QO 2024-05-27, QO 2024-06-10, QO 2024-06-24, QO 2024-07-08, QO 2024-07-22, QO 2024-08-05, QO 2024-08-19, QO 2024-09-02, QO 2024-09-16, QO 2024-09-30, QO 2024-10-14

      TL;DR:

      A quick win for improved usability of the internal Deferred helper, reducing memory usage and runtime overhead. No externally visible impact (unless Deferred is used in a particularly hot location, in which case a mild perf benefit may be seen in benchmarks).


      The Deferred template is used to lazily compute a value.

      The template params will often be deduced:

      auto foo = Deferred([](){...});
      

      However, CTAD does not apply to member variables.

      struct Foo {
          Deferred<???> member = [](){};
      };
      

      Lambdas cannot be conveniently named in the above case as the parameter type.
      Non-capturing lambdas can, however, decay to function pointers.

      struct Foo {
          Deferred<int(*)()> member = [](){return 1;};
      };
      

      Users do not always enjoy function pointer types. For convenience, we should add DeferredFnPtr (through analogy to the existing DeferredFn, wrapping an std::function).

      struct Foo {
          DeferredFn<int, std::string> member = [&](std::string foo){return someCapture;};
          DeferredFnPtr<int, std::string> member = [](std::string foo){return 1;};
      };
      

      However, both of these pay additional cost to store an std::function or function ptr respectively, and may inhibit some optimisations.

      See godbolt for size cost of std::function.

      Returning to the case of:

      auto foo = Deferred([](){...});
      

      As the lambda has no capture, it will be zero sized. By using [[no_unique_address]] (or other common solutions), there will be no space overhead for storing an initializer function in this case. It would be ideal for this to be possible in a member variable, without inconveniences such as:

      struct Foo {
          static constexpr auto _foo =  [](){return 1;};
          Deferred<decltype(_foo)> member = foo;
      };
      

      One potential solution is the use of C++20 support for stateless lambdas in unevaluated contexts; that is, extend Deferred to support:

      struct Foo {
          DeferredParam<[]() {return 1;}> member;
      };
      

      Another option is to assess current usages and expand upon the abstraction.

      For example, this is a common pattern:

      struct Foo {
          DeferredFn<int> member = [](){return someGlobalAtomic.loadRelaxed();};
      };
      

      As noted, this pays an unnecessary cost by using an std::function for a compile-time known method, which is not rebound during the life of the DeferredFn in most cases. Recognising this, a more specific type could be added:

      struct Foo {
          CachedGlobal<someGlobalValue> member;
      };
      

      This could be trivially implemented using DeferredParam.

      This cleanly expresses the author's intent - this member will always read from, then cache this specific value. The interface could be tailored to common usages; e.g., specialised for Atomic to add matching methods to control the memory ordering used when loading the global value.

            Assignee:
            james.harrison@mongodb.com James Harrison
            Reporter:
            james.harrison@mongodb.com James Harrison
            Votes:
            0 Vote for this issue
            Watchers:
            6 Start watching this issue

              Created:
              Updated: