diff --git a/includes/cache.inc b/includes/cache.inc
index 8666874..1a8ab18 100644
--- a/includes/cache.inc
+++ b/includes/cache.inc
@@ -506,3 +506,177 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
     return empty($result);
   }
 }
+
+/**
+ * Interface for allowing ArrayObject to be used as a caching wrapper.
+ *
+ * Extend CacheArrayObject rather than implementing this interface. This
+ * mainly exists to document and enforce the additional methods required in
+ * CacheArrayObject.
+ *
+ * @see CacheArrayObject
+ */
+interface CacheArrayObjectInterface {
+
+  /**
+   * Resolve a cache miss.
+   *
+   * When an offset is not found in the object,  this is treated as a cache
+   * miss. This method allows classes implementing the interface to look up
+   * the actual value and allow it to be cached.
+   *
+   * @param $offset
+   *   The offset that was requested.
+   */
+  public function resolveCacheMiss($offset);
+
+  /**
+   * Add an offset and value to the ArrayObject cache.
+   *
+   * @param $offset
+   *   The array offset that was request.
+   * @param $value
+   *   The value to set.
+   * @param $request_only
+   *   Optional boolean to force the key to be persisted only for the request.
+   *   Defaults to FALSE.
+   */
+  public function persist($offset, $value, $request_only = FALSE);
+
+  /**
+   * Write to the persistent cache.
+   *
+   * @param $cid
+   *   The cache ID.
+   * @param $bin
+   *   The cache bin.
+   * @param $data
+   *   The data to write to the persistent cache.
+   * @param $lock
+   *   Whether to acquire a lock before writing to cache.
+   */
+  public function set($cid, $data, $bin, $lock = TRUE);
+}
+
+/**
+ * Extends ArrayObject to enable it to be used as a caching wrapper.
+ *
+ * This class should be extended by systems that need to cache large amounts
+ * of data and have it represented as an array to calling functions. These
+ * arrays can become very large, so ArrayObject is used to allow different
+ * strategies to be used for caching internally (lazy loading, building caches
+ * over time etc.). This can dramatically reduce the amount of data that needs
+ * to be loaded from cache backends on each request, and memory usage from
+ * static caches of that same data.
+ *
+ * Note that array_* functions do not work with ArrayObject.
+ *
+ * By default, the class accounts for caches where calling functions might
+ * request keys in the array that won't exist even after a cache rebuild. This
+ * prevents situations where a cache rebuild would be triggered over and over
+ * due to a 'missing' item. These cases are stored internally as a value of
+ * NULL. This means that the offsetGet() and offsetExists() methods
+ * must be overridden if caching an array where the top level values can
+ * legitimately be NULL, and where $object->offsetExists() needs to correctly
+ * return (equivalent to array_key_exists() vs. isset()). This should not
+ * be necessary in the majority of cases.
+ *
+ * Classes extending this class will usually need to override at least the
+ * resolveCacheMiss() method to have a working implementation.
+ *
+ * @see ThemeRegistry
+ */
+class CacheArrayObject extends ArrayObject implements CacheArrayObjectInterface {
+
+  /**
+   * A cid to pass to cache_set() and cache_get().
+   */
+  private $cid;
+
+  /**
+   * A bin to pass to cache_set() and cache_get().
+   */
+  private $bin;
+
+  /**
+   * An array of keys to add to the cache at the end of the request.
+   */
+  protected $add_keys = array();
+
+  /**
+   * Constructor.
+   *
+   * @param $cid
+   *   The cid for the array being cached.
+   * @param $bin
+   *   The bin to cache the array.a
+   */
+  function __construct($cid, $bin) {
+    $this->cid = $cid;
+    $this->bin = $bin;
+
+    if ($cached = cache_get($this->cid, $this->bin)) {
+      parent::__construct($cached->data);
+    }
+    else {
+      parent::__construct(array());
+    }
+  }
+
+  public function offsetExists($offset) {
+    if (!parent::offsetExists($offset)) {
+     $this->resolveCacheMiss($offset);
+    }
+    return parent::offsetGet($offset) !== NULL;
+  }
+
+  public function offsetGet($offset) {
+    if (!parent::offsetExists($offset)) {
+      $this->resolveCacheMiss($offset);
+    }
+    return parent::offsetGet($offset);
+  }
+
+  public function persist($offset, $value, $request_only = FALSE) {
+    parent::offsetSet($offset, $value);
+    $this->add_keys[] = $offset;
+  }
+
+  public function resolveCacheMiss($offset) {
+    // Setting the offset to NULL causes offsetGet() and offsetSet()
+    // to act as if the item does not exist in the array. Most implementations
+    // will want to override this method to consult the structure being cached
+    // and set offset to the value if it exists there.
+    $this->persist($offset, NULL);
+  }
+
+  public function set($cid, $data, $bin, $lock = TRUE) {
+    $lock_name = $cid . ':' . $bin;
+    if (!$lock || lock_acquire($lock_name)) {
+      if ($cached = cache_get($cid, $bin)) {
+        $data = array_merge($cached->data, $data);
+      }
+      cache_set($cid, $data, $bin);
+      if ($lock) {
+        lock_release($lock_name);
+      }
+    }
+  }
+
+  public function __destruct() {
+    if (!empty($this->add_keys)) {
+      // Since this method merges with the existing cache entry if it exists,
+      // ensure that only one process can update the cache item at any one time.
+      // This ensures that different requests can't overwrite each others'
+      // partial version of the cache and should help to avoid stampedes.
+      // When a lock cannot be acquired, the cache will not be written by
+      // that request. To implement locking for cache misses, override
+      // __construct().
+      $data = array();
+      foreach ($this->add_keys as $key) {
+        $data[$key] = parent::offsetGet($key);
+      }
+      $this->set($this->cid, $data, $this->bin);
+    }
+  }
+}
diff --git a/includes/common.inc b/includes/common.inc
index fbad974..885a7f1 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2333,7 +2333,7 @@ function l($text, $path, array $options = array()) {
     // rendering.
     if (variable_get('theme_link', TRUE)) {
       drupal_theme_initialize();
-      $registry = theme_get_registry();
+      $registry = theme_get_registry(FALSE);
       // We don't want to duplicate functionality that's in theme(), so any
       // hint of a module or theme doing anything at all special with the 'link'
       // theme hook should simply result in theme() being called. This includes
diff --git a/includes/theme.inc b/includes/theme.inc
index c211248..4fa1f5d 100644
--- a/includes/theme.inc
+++ b/includes/theme.inc
@@ -237,18 +237,33 @@ function _drupal_theme_initialize($theme, $base_theme = array(), $registry_callb
 /**
  * Get the theme registry.
  *
+ * @param $complete
+ *   Optional boolean to indicate whether to return the complete theme registry
+ *   array or an instance of the ThemeRegistry class. If TRUE, the complete
+ *   theme registry array will be returned. This is useful if you want to
+ *   foreach over the whole registry, use array_* functions or inspect it in a
+ *   debugger. If FALSE, an instance of the ThemeRegistry class will be
+ *   returned, this provides an ArrayObject which allows it to be accessed
+ *   with array syntax, isset() and empty(), and it should be more
+ *   lightweight than the full registry. Defaults to TRUE.
+ *
  * @return
- *   The theme registry array if it has been stored in memory, NULL otherwise.
+ *   The complete theme registry array, or an instance of the ThemeRegistry
+ *   class.
  */
-function theme_get_registry() {
-  static $theme_registry = NULL;
+function theme_get_registry($complete = TRUE) {
+  static $theme_registry = array();
+  $key = (int) $complete;
 
-  if (!isset($theme_registry)) {
+  if (!isset($theme_registry[$key])) {
     list($callback, $arguments) = _theme_registry_callback();
-    $theme_registry = call_user_func_array($callback, $arguments);
+    if (!$complete) {
+      $arguments[] = FALSE;
+    }
+    $theme_registry[$key] = call_user_func_array($callback, $arguments);
   }
 
-  return $theme_registry;
+  return $theme_registry[$key];
 }
 
 /**
@@ -268,7 +283,7 @@ function _theme_registry_callback($callback = NULL, array $arguments = array())
 }
 
 /**
- * Get the theme_registry cache from the database; if it doesn't exist, build it.
+ * Get the theme_registry cache; if it doesn't exist, build it.
  *
  * @param $theme
  *   The loaded $theme object as returned by list_themes().
@@ -277,23 +292,34 @@ function _theme_registry_callback($callback = NULL, array $arguments = array())
  *   oldest first order.
  * @param theme_engine
  *   The name of the theme engine.
+ * @param $complete
+ *   Whether to load the complete theme registry or an instance of the
+ *   ThemeRegistry class.
+ *
+ * @return
+ *   The theme registry array, or an instance of the ThemeRegistry class.
  */
-function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
-  // Check the theme registry cache; if it exists, use it.
-  $cache = cache_get("theme_registry:$theme->name", 'cache');
-  if (isset($cache->data)) {
-    $registry = $cache->data;
+function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL, $complete = TRUE) {
+  if ($complete) {
+    // Check the theme registry cache; if it exists, use it.
+    $cache = cache_get("theme_registry:$theme->name", 'cache');
+    if (isset($cache->data)) {
+      $registry = $cache->data;
+    }
+    else {
+      // If not, build one and cache it.
+      $registry = _theme_build_registry($theme, $base_theme, $theme_engine);
+      // Only persist this registry if all modules are loaded. This assures a
+      // complete set of theme hooks.
+      if (module_load_all(NULL)) {
+        _theme_save_registry($theme, $registry);
+      }
+    }
+    return $registry;
   }
   else {
-    // If not, build one and cache it.
-    $registry = _theme_build_registry($theme, $base_theme, $theme_engine);
-   // Only persist this registry if all modules are loaded. This assures a
-   // complete set of theme hooks.
-    if (module_load_all(NULL)) {
-      _theme_save_registry($theme, $registry);
-    }
+    return new ThemeRegistry($theme, $base_theme, $theme_engine);
   }
-  return $registry;
 }
 
 /**
@@ -313,6 +339,81 @@ function drupal_theme_rebuild() {
 }
 
 /**
+ * Builds the run-time theme registry.
+ *
+ * This class extends the CacheArrayObject class to allow the theme registry to be
+ * accessed as a complete registry, while internally caching only the parts of
+ * the registry that are actually in use on the site. On cache misses the
+ * complete theme registry is loade and used to update the run-time cache.
+ */
+class ThemeRegistry Extends CacheArrayObject {
+  private $theme;
+  private $base_theme;
+  private $theme_engine;
+  private $unregistered_cid;
+  private $persistable;
+
+  function __construct($theme, $base_theme = NULL, $theme_engine = NULL) {
+    // Make the arguments available to the rest of the class.
+    $this->theme = $theme;
+    $this->base_theme = $base_theme;
+    $this->theme_engine = $theme_engine;
+    // Maintain a global cache item for registered theme hooks.
+    $this->cid = 'theme_registry:runtime:' . $theme->name;
+    $this->bin = 'cache';
+    // Maintain a per-page cache item for hooks that aren't registered. This
+    // will usually be un-implemented theme suggestions. Suggestions can be
+    // passed into theme() based on any value, so the per-page cache prevents
+    // any one cache item from growing to large.
+    $this->unregistered_cid = 'theme_registry:unregistered:' . $theme->name . ':' . hash('sha256', current_path());
+
+    $data = array();
+    if ($cached = cache_get($this->cid, $this->bin)) {
+      $data = $cached->data;
+    }
+    if ($cached = cache_get($this->unregistered_cid, $this->bin)) {
+      $data = array_merge($data, $cached->data);
+    }
+    ArrayObject::__construct($data);
+  }
+
+  public function resolveCacheMiss($offset) {
+    // If there is a cache miss and the registry is requested before
+    // all modules have been loaded, ensure that there is no chance
+    // of a partial registry being written back to cache.
+    if (!isset($this->persistable)) {
+      $this->persistable = module_load_all(NULL);
+    }
+    $complete_registry = theme_get_registry();
+    $value = isset($complete_registry[$offset]) ? $complete_registry[$offset] :  NULL;
+    $this->persist($offset, $value, $this->persistable);
+  }
+
+  public function __destruct() {
+    if (!empty($this->add_keys)) {
+      $registered = array();
+      $unregistered = array();
+      foreach ($this->add_keys as $offset) {
+        $value = $this->offsetGet($offset);
+        if ($value === NULL) {
+          $unregistered[$offset] = $value;
+        }
+        else {
+          $registered[$offset] = $value;
+        }
+      }
+      if (!empty($registered)) {
+        $this->set($this->cid, $registered, $this->bin);
+      }
+      if (!empty($unregistered)) {
+        // Don't bother locking for the per-page cache.
+        $this->set($this->unregistered_cid, $unregistered, $this->bin, FALSE);
+      }
+    }
+  }
+}
+
+/**
  * Process a single implementation of hook_theme().
  *
  * @param $cache
@@ -760,7 +861,7 @@ function theme($hook, $variables = array()) {
 
   if (!isset($hooks)) {
     drupal_theme_initialize();
-    $hooks = theme_get_registry();
+    $hooks = theme_get_registry(FALSE);
   }
 
   // If an array of hook candidates were passed, use the first one that has an
diff --git a/modules/contextual/contextual.module b/modules/contextual/contextual.module
index 0d6b625..2716ba2 100644
--- a/modules/contextual/contextual.module
+++ b/modules/contextual/contextual.module
@@ -82,16 +82,12 @@ function contextual_element_info() {
  * @see contextual_pre_render_links()
  */
 function contextual_preprocess(&$variables, $hook) {
-  static $hooks;
-
   // Nothing to do here if the user is not permitted to access contextual links.
   if (!user_access('access contextual links')) {
     return;
   }
 
-  if (!isset($hooks)) {
-    $hooks = theme_get_registry();
-  }
+  $hooks = theme_get_registry(FALSE);
 
   // Determine the primary theme function argument.
   if (!empty($hooks[$hook]['variables'])) {
