Retrieve information from a library without wasting runtime if not required

I am the author of a library with a number of optimization algorithms in which I have already invested a lot of profiling / tuning. I am currently writing front-end programs for this library.

At the moment, library routines themselves are pretty black boxes. Let's consider the method line by line bool fit(vector_t const& data, double targetError)

. They work and do what I want them to do, but for interfaces, a little runtime information would be nice. For example, it would be nice if data such as "current error" or "number of iterations to the left" were displayed. I don't need a simple template if (verbose) cerr << "Info\n";

as the library should be equally applicable in a GUI environment.

I deliberately wrote that I could, because I want to influence this information as little as possible. How can I define and implement an interface that

  • Gives an object with runtime information when registering an observer
  • has minimal execution time with respect to the algorithm if an information object is emitted, and
  • has no launch time investment if the registered observer is not registered?

In principle, the time required to perform this optional introspection should be as low as possible and close to zero if introspection is not required. How can this be achieved? Are there libraries for this? (I think the answer is yes and is probably hidden in the Boost project, but didn't know what to look for ...)

+3


source to share


2 answers


Just collect all the costs to compile time!

The easiest way:

template<bool logging>
bool fit(vector_t const& data, double targetError)
{
    // ... computations ...
    if (logging) {
        std::cout << "This is log entry\n";
    }
    // ... computations ...
}

      

Using:

fit<true>(data, epsilon); // Will print logs.
fit<false>(data, epsilon); // Will not print logs with zero overhead.

      

Almost any compiler nowadays will optimize all such compilation checks.

A more flexible approach is to pass the logger as a template parameter:



class ConsoleLogger
{
public:
    template<typename... Args>
    static void log(Args... args) {
        // Print args using compile-time recursion.
        // Anyway, this prototype is only an example.
        // Your may define any logging interface you wish.
    }
};

class FileLogger
{
    // Some implementation of log() ...
};

class RemoteCloudLogger
{
    // Some implementation of log() ...
};

class NullLogger
{
    template<typename... Args>
    static void log(Args... args) {
        // Intentionally left blank. Any calls will be optimized out.
    }
};

template<typename Logger>
bool fit(vector_t const& data, double targetError)
{
    // ... computations ...
    Logger::log("Current error: ", currentError);
    Logger::log("Iterations passed: ", i);
    // ... computations ...
}

      

Using:

fit<ConsoleLogger>(data, epsilon); // Will log to console.
fit<RemoteCloudLogger>(data, epsilon); // Will log to a file on remote cloud server.
fit<NullLogger>(data, epsilon); // Any calls to Logger::log will be optimized out, yielding zero overhead.

      

Obviously, you can write a log class that will collect all the recorded information into a structure with introspection data.

For example, you can define an interface fit

like this:

template<typename Logger>
bool fit(vector_t const& data, double targetError, IntrospectionData& data)
{...}

      

and only define two logging classes: IntrospectionDataLogger

and NullLogger

, the methods log

take a structure reference IntrospectionData

. Again, the last class contains empty methods that will be thrown out by your compiler.

+2


source


You can allocate a structure on the stack and pass the reference to the observer. The observer can then do whatever is necessary for the structure, for example. copy it, print it, display it in the GUI, etc. Also, if you only want to keep track of one or two properties, you can simply pass them as separate arguments.

class Observer
{
...
public:
     virtual void observe(MyInfoStruct *info) = 0;
}

...
if(hasObservers()) // ideally inline
{
    MyInfoStruct info = {currentError, bla, bla};
    for(observer : observers)
    {
        observer->observe(&info);
    }
}

      

Thus, there should be no overhead if there are no observers other than the operator if

. The overhead for emitting information should prevail in virtual calls to observers.

Further optimization: You might want to delineate the presumably cold observer iteration code into a separate function to improve code locality for the hot path.



If you have frequent virtual calls in your code, consider adding an "watched" version of the interface and try to observe there and omit it completely from the original version. If the observable version can easily be injected once after placing the observer, you can omit the check for redundant observers. If you just want to keep track of the arguments to a function, this is easy to do by making the Observable version just a redirect to the original, however, if you want to keep track of the information while the algorithm is running, this may not be appropriate.

Depending on what you want to track, it might be possible to write this as a template:

struct NoObserverDispatch {
     bool hasObservers() {return false;}
     void observe(MyInfoStruct *) {}
};

struct GenericObserverDispatch {
     bool hasObservers() {return !observers.isEmpty();}
     void observe(MyInfoStruct *) { for (obs : observers) obs->observe(info); }
     private:
     vector<unique_ptr<Observer> > observers;
};

template<typename ObserverDispatch>
class Fitter
{
     ObserverDispatch obs;
     virtual bool fit(vector_t const& data, double targetError)
     {
         ...
         if(obs.hasObservers()) { // will be optimized away unless needed
             MyInfoStruct info = {currentError, bla, bla};
             obs.observe(&info);
         }
     }
};

      

Obviously this assumes that you can replace Fitter

whenever an observer is placed. Another option would be to let the user select an instance of the template, but this may be less clean and has a drawback that you will have to submit code.

0


source







All Articles