Dynamic Dispatch

Modia3D uses several abstract types in structs in order to store information about different types of joints, shapes, collision response laws etc. Several functions are used to operate on these types. A direct implementation would lead to dynamic dispatch, so the functions to be called and the types to be operated on, would be determined at run-time (and not at compile time). This leads to slow simulation and a lot of memory allocation during simulation.

In this forum discussion a similar issue is discussed for a tiny use case and three solutions are compared: (1) dynamic dispatch, (2) dictionary lookup, and (3) if-clauses. In this simple example version (3) is about a factor of 15 faster as (1) and allocates 200 bytes, whereas (1) allocates 1.5 Mbyte.

In the rest of this section the technique is sketched that is used in Modia3D to avoid (a) dynamic dispatch and (b) access of abstract types during simulation:

The Object3D struct holds all information about an Object3D object:

@enum JointKind FixKind RevoluteKind PrismaticKind ...
@enum ShapeKind NoShapeKind SphereKind BoxKind CylinderKind ...

mutable struct Object3D
    ...
    # Relative motion parent/Object3D
    joint::AbstractJoint               # ::Fix, ::Revolute, ...

    # Efficient access of joint properties
    jointKind::JointKind               # Kind of joint
    ndof::Int                          # Number of degrees of freedom
    canCollide::Bool                   # = false, if no collision parent/Object3D

    # Feature associated with Object3D
    feature::AbstractObject3DFeature   # ::EmptyObject3DFeature ::Scene, ::Visual, ::Solid

    # Efficient access of feature properties
    shapeKind::Shapes.ShapeKind        # Kind of shape
    shape::Modia3D.AbstractShape
    visualMaterial::Shapes.VisualMaterial
    ...
end

In particular it holds abstract types for the joint and the feature associated with the Object3D. During initialization, the properties of joint and feature are copied to storage locations that have a concrete type.

All operations on the abstract types are implemented with if-clauses and only access concrete types. For example, Support points needed to determine the shortest distances between colliding Object3D's are computed with:

function supportPoint(obj::Object3D, e::SVector{3,Float64})::SVector{3,Float64}
    shapeKind                = obj.shapeKind   # type Int
    solid::Modia3D.Solid     = obj.feature
    collisionSmoothingRadius = solid.collisionSmoothingRadius  # type Float64

    if shapeKind == Modia3D.SphereKind
        return supportPoint_Sphere(obj.shape, obj.r_abs, obj.R_abs, e)
    elseif shapeKind == Modia3D.EllipsoidKind
        return supportPoint_Ellipsoid(obj.shape, obj.r_abs, obj.R_abs, e)
    elseif shapeKind == Modia3D.BoxKind
        return supportPoint_Box(obj.shape, obj.r_abs, obj.R_abs, e, collisionSmoothingRadius)
    ...
    end
end

Note, a function such as supportPoint_Sphere(..) requires that its first argument is an instance of the concrete type Sphere. Due to the if-clause, this is guaranteed. Julia will just make a cheap run-time check that this requirement is fulfilled. The concrete function (supportPoint_Sphere) is translated when llvm code is generated.