Search code examples
rustdynamic-dispatch

How to express this concept without dynamic dispatch?


I am building a simulation (the coding equivalent of a model train set). It is a simulated economy with various economic agents interacting with each other. The main mode of interaction between economic agents is a transaction. At each "tic", each agent generates a list of zero or more proposed transactions (such as buying food). At each "toc" all the counter-parties process the proposed transactions that have been targeted at them in random order so that no biases are introduced. In these snippets a proposed transaction is represented as a u32.

My goal is to simulate as many of these economic agents as possible so performance is key. I am new to rust (or any kind of low level language for that matter) and my understanding from reading the rust book is if I want maximum performance then use "zero cost abstractions" and avoid dynamic dispatch.

So with that out the way I have come up with the following 3 approaches.

Option 1

trait EconomicAgent {
    fn proposed_transactions(&self) -> Vec<u32>;
}

struct Person {
    health:f64,
    energy:f64,
    nutrition:f64,
    money:f64,
    food:f64
}

impl EconomicAgent for Person {
    fn proposed_transactions(&self) -> Vec<u32> {
        vec![1, 2, 3]
    }
}

struct FoodStore {
    money:f64,
    food:f64
}

impl EconomicAgent for FoodStore {
    fn proposed_transactions(&self) -> Vec<u32> {
        vec![4, 5, 6]
    }
}

A person and a food store are different types that implement the EconomicAgent trait. I can then iterate over a vector of trait objects to get a list of proposed transactions. Each call is dynamically dispatched, I believe.

Option 2

enum EconomicAgent2 {
    Person(Person),
    FoodStore(FoodStore)
}

impl EconomicAgent2 {
    fn proposed_transactions(&self) -> Vec<u32> {
        match self{
            EconomicAgent2::Person(person) => person.proposed_transactions(),
            EconomicAgent2::FoodStore(food_store) => food_store.proposed_transactions()
        }
    }
}

Here, an EconomicAgent is not a trait, but rather an enum and, well you can see how it works.

Option 3

const HEALTH_INDEX : u8 = 0;
const ENERGY_INDEX : u8 = 1;
const NUTRITION_INDEX : u8 = 2;
const MONEY_INDEX : u8 = 3;
const FOOD_INDEX : u8 = 4;

enum EconomicAgentTag {
    Person,
    FoodStore
}
struct EconomicAgent3 {
    tag: EconomicAgentTag,
    resources:[f64; 5],
    proposed_transactions: Box<fn(&EconomicAgent3) -> Vec<u32>>
}

fn new_person() -> EconomicAgent3 {
    EconomicAgent3 {
        tag: EconomicAgentTag::Person,
        resources: [0.0,0.0,0.0,0.0,0.0],
        proposed_transactions: Box::new(|_| vec![1, 2, 3])
    }
}

fn new_food_Store() -> EconomicAgent3 {
    EconomicAgent3 {
        tag: EconomicAgentTag::FoodStore,
        resources: [0.0,0.0,0.0,0.0,0.0],
        proposed_transactions: Box::new(|_| vec![4, 5, 6])
    }
}

Here an economic agent is more abstract representation.

Now imagine that there a many different types of economic agents (banks, mines, farms, clothing stores etc). They all interact by proposing and accepting transactions. Option 1 seems to suffer from dynamic dispatch. Option 2 seems to be my own version of dynamic dispatch via a match expression so is probably no better, right? Option 3 seems like it should be the most performant but does not really allow much cognitive ease on the part of the programmer.

So finally the questions:

  1. Clearly dynamic dispatch is involved in option 1. What about options 2 and 3?
  2. Which is expected to be most performant? Note I am not really in a position to do testing as the full idea (only on paper right now) is obviously more complex than these snippets and the choice now will affect the entire structure for the whole project.
  3. What would be an idiomatic choice here?

Solution

  • All your options use dynamic dispatch or branches in one way or another to call the right function for each element. The reason is that you are mixing all the agents into a single place, which is where the different performance penalties come from (not just the indirect calls or branches, but also cache misses etc.).

    Instead, for a problem like this, you want to separate the different "agents" into separate, independent "entities". Then, to reuse code, you will want to factor out "components" for which subsets of them are iterated by "systems".

    This is what is usually called an "Entity-Component-System" (ECS) of which there are many models and implementations. They are typically used by games and other simulations.

    If you search for ECS you will find many questions, articles and so on about it and the different approaches.