Don't forget that lexicals can be subject to some nice optimizations. A lexical variable that is loaded with a constant and never assigned can be constant-propagated, and disappear. Lexical variables that aren't captured in closures can be moved into registers.
There are more opportunities to do these kinds of things with lexical variables compared to dynamic, because we don't know what happens with a dynamic variable when some function is called that we know nothing about. If a scope calls any functions at all whose definitions are not available or cannot be analyzed, then it has to be assumed that any dynamic variable used in the scope is also accessed and mutated by those functions, which defeats these optimizations.
When native code is generated, VM temporary registers can be allocated to real machine registers; any variables that have been moved to VM temp registers benefit.
Your observation is spot on that the discipline for saving and restoring registers is like binding and unbinding dynamic variables. But there is only a fixed, relatively small number of registers, which makes it practical to save and restore them transparently at thread context switches. This means that even under multi-threading, procedures can just save and restore registers as if everything were single-threaded.
Dynamic variables under threading cannot be treated by saving and restoring. That would require each thread to have its own, which would be expensive to switch. (The top-level/global bindings of dynamics have to be seen by all threads, also, which means that if threads have their own file of dynamic variables, there nevertheless has to be some indirection so that threads can share the global bindings.)
Under a deep binding strategy for dynamic scope, you have an environment chain of scopes for dynamic variables. That scope is rooted at a single pointer, which can be turned into a thread context: so one word of thread context switches dynamic scopes. With deep binding, some optimizations are possible: when a scope is entered that references a dynamic variable, that variable can be looked up in the deep environment to retrieve its value cell, and that value cell can be forwarded into the lexical frame or a register. All the accesses of the variable in the scope refer to that register, which provides a quick indirection to the variable, without the environmental lookup having to be repeated. Dynamic variables can also be forwarded to aliases at the top of the environment for faster lookup next time. We cannot move a dynamic variable from a deeper frame to the top frame, because that would make it disappear from some upstream scope we have to return to. But we can plant an alias which references the same location.
now that i understand your comment after a few readings, these are all good points (and, for the sake of those following along, you have a lot more expertise in this than i do)
There are more opportunities to do these kinds of things with lexical variables compared to dynamic, because we don't know what happens with a dynamic variable when some function is called that we know nothing about. If a scope calls any functions at all whose definitions are not available or cannot be analyzed, then it has to be assumed that any dynamic variable used in the scope is also accessed and mutated by those functions, which defeats these optimizations.
When native code is generated, VM temporary registers can be allocated to real machine registers; any variables that have been moved to VM temp registers benefit.
Your observation is spot on that the discipline for saving and restoring registers is like binding and unbinding dynamic variables. But there is only a fixed, relatively small number of registers, which makes it practical to save and restore them transparently at thread context switches. This means that even under multi-threading, procedures can just save and restore registers as if everything were single-threaded. Dynamic variables under threading cannot be treated by saving and restoring. That would require each thread to have its own, which would be expensive to switch. (The top-level/global bindings of dynamics have to be seen by all threads, also, which means that if threads have their own file of dynamic variables, there nevertheless has to be some indirection so that threads can share the global bindings.)
Under a deep binding strategy for dynamic scope, you have an environment chain of scopes for dynamic variables. That scope is rooted at a single pointer, which can be turned into a thread context: so one word of thread context switches dynamic scopes. With deep binding, some optimizations are possible: when a scope is entered that references a dynamic variable, that variable can be looked up in the deep environment to retrieve its value cell, and that value cell can be forwarded into the lexical frame or a register. All the accesses of the variable in the scope refer to that register, which provides a quick indirection to the variable, without the environmental lookup having to be repeated. Dynamic variables can also be forwarded to aliases at the top of the environment for faster lookup next time. We cannot move a dynamic variable from a deeper frame to the top frame, because that would make it disappear from some upstream scope we have to return to. But we can plant an alias which references the same location.