Downcasting overhead?

Hi .

I need store many different type, i found anymap
Its a hashmap store value as Any,

How much is overhead of downcasting ??

Which is better ??

  1. use a raw pointor in my own hashmap
  2. use anymap ( that use downcasting )

Unless your program is incredibly performance sensitive, it's probably better to use anymap instead of raw pointers. The performance difference almost certainly isn't worth the risk of causing undefined behavior by deferencing something as the wrong type.

If you really need that performance, and you can be 100% sure the value is of the right type, then you could use pointers, but chances are anymap is better

Side note: it seems anymap is unmaintained (it hasn't had a commit in over 3 years, or a release in almost 5) so it might be worth looking if there's any other crates that do something similar (although I am unsure if there are any, would an ECS crate work for you?)

It is pretty trivial to write such map yourself.
downcast overhead can be avoided with _unchecked methods.
You can also assume that keys (types) are unique hence you need to use custom hasher to store typeid as u64 key without wasting CPU to compute hash of u64 type id

I think anymap does both (check source code)

Do note that you still has overhead from dereferencing in general, but this cannot be avoided due to nature of pointers

P.s. I also has my own type map implementation, you could use it alternatively as guidence to write your own
https://github.com/DoumanAsh/type-map

1 Like

What do you mean? There's no downcast_{ref|mut}_unchecked methods on Any (although maybe there should be :thinking:)

Oh you're right, my bad, for some reason I thought there are.
Looking at my code I just work it around by simply hinting compiler that downcast cannot fail:

match ptr.downcast_mut() {
    Some(res) => res,
    None => unsafe {
        core::hint::unreachable_unchecked()
    },
}

I choice rust for real time application , performance is crucial but, this component is not
in hotcode, this is a task registry,

We can actually check how much overhead a downcast incurs by using some specially crafted functions and looking at the assembly.

There are two parts to downcasting, a) calling some function to get a type's ID and casting the reference if it matches our type, and b) the actual type_id() function automatically implemented by the compiler.

I'm using the following code because I know it will generate assembly for both steps without optimising it away to nothing.

use std::any::{Any, TypeId};

/// The actual downcasting.
pub fn downcast_to_string(value: &dyn Any) -> Option<&String> {
    value.downcast_ref()
}

pub fn use_any_string() -> TypeId {
    let s = String::new();
    get_type_id(&s)
}

/// A shim function which will act as an optimisation barrier, forcing the 
/// compiler to emit String's type_id() method so we can see what it does.
#[inline(never)]
pub fn get_type_id(value: &dyn Any) -> TypeId {
    value.type_id()
}

(playground)

If you switch to "release" mode and ask the playground to show assembly (see the triple-dot menu in the top-left corner), we can see the <String as Any>::type_id() implementation is a simple function that returns a hard-coded value (the type ID used in the compiler).

<T as core::any::Any>::type_id:
	movabsq	$61489947429405626, %rax
	retq

Now we can look at the machine code generated for downcast_to_string(). The compiler will have inlined the downcast() call for us, so all the instructions we see actually belong to Any's downcast() method.

playground::downcast_to_string:
	pushq	%rbx
	movq	%rdi, %rbx
	callq	*24(%rsi)
	xorl	%ecx, %ecx
	movabsq	$61489947429405626, %rdx
	cmpq	%rdx, %rax
	cmoveq	%rbx, %rcx
	movq	%rcx, %rax
	popq	%rbx
	retq

Now, you need to understand that &dyn Any is actually a pair of pointers (hence the "fat pointer" name), one pointer points to the actual value and another that points to a "vtable" containing extra metadata and function pointers for each method on the Any trait. In downcast_to_string(), these pointers are passed in via registers.

What downcast_to_string() does is call a function at offset 24 from the vtable pointer in %rsi and compare the number it returns with the constant $61489947429405626 (presumably the type ID for String). The xorl instruction clears our %ecx register so cmoveq can conditionally overwrite it with the data pointer in %rbx if the comparison succeeds. We then do a bit of register shuffling and return either the String pointer or null to the caller.

To figure out what is at offset 24, we can look the Any vtable generated for String.

It actually contains 4 pointer sized fields, a function for dropping the String, the size and alignment (stored as a single byte constant), and a function pointer for the Any trait's one method, <T as core::any::Any>::type_id. The type_id() method is the 4th item, meaning it has 3 pointer-sized values before it (3 * 8 = 24 bytes), therefore callq *24(%rsi) will invoke type_id() when %rsi contains a pointer to the vtable.

.L__unnamed_2:
	.quad	core::ptr::drop_in_place<alloc::string::String>
	.asciz	"\030\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
	.quad	<T as core::any::Any>::type_id

TL;DR: downcasting through Any is a handful of instructions and a single dynamic function call. You probably won't notice/care in 99.99% of cases.

6 Likes

They've just been added in nightly:

2 Likes

Awesome!

I was just writing a post on IRLO about the idea so thanks for saying that before I made a bit of a fool of myself :stuck_out_tongue:

1 Like

Thank you for take your time

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.