>>56LuaJIT and the various Javascript JITs use runtime information to suss out the "hidden types" common in dynamic language programs. It might look something like this:
1. Our instruments indicate this function is being called a lot. It returns one object, let's see what happens to it.
2. Our instruments indicate that 99.99% of these objects will never have any fields added or removed from them. Looks like a static type.
3. Let's change this function so that it creates a vector with a fixed number of slots, and replace every hash lookup with a direct index into those slots.
4. If a field is added or removed from one of these objects, we back out and convert it into a hash table.
5. If this happens enough times, we should consider recompiling the original function.
IIRC LuaJIT goes a step further, assumes tables will be static by default, and converts any keys visible in the program text into a static lookup from the get go, but you get the idea. Storing everything in hash tables is the simplest, most general way to implement a dynamic language, but you absolutely can optimize it further if you're willing to complicate the implementation and add checks so that the optimization can be reverted if your assumptions turn out to be false.
The downside is increased runtime overhead vs. an ahead of time compiled language. The upside is that a good JIT can take advantage of information that might not be available to a static compiler. i.e. if a function is called thousands of times with one of its inputs never changing, and this input was read from user input and not statically knowable, an AoT compiler would have no choice but to make use of a general function suitable for all inputs, while a JIT could still produce a specialized version of the function for that input. The sweet spot between strictly AoT C and the crazy JITs of Lua and JS would be something like Common Lisp, where the programmer has access to the compiler at runtime and it's their own responsibility to compile specialized versions of functions.