code: public void cacheVectors() {
          if (originalThunks != null) return;
  
          originalThunks = new NDArray<>(getDimensions());
  
          // OPTIMIZATION:
          // Rather than use the standard iterator, which creates lots of int[] arrays on the heap, which need to be GC'd,
          // we use the fast version that just mutates one array. Since this is read once for us here, this is ideal.
          Iterator<int[]> fastPassByReferenceIterator = fastPassByReferenceIterator();
          int[] assignment = fastPassByReferenceIterator.next();
          while (true) {
              Supplier<ConcatVector> originalThunk = getAssignmentValue(assignment);
              originalThunks.setAssignmentValue(assignment, originalThunk);
  
              // Construct a new, thinner closure around the cached value
              ConcatVector result = originalThunk.get();
              setAssignmentValue(assignment, () -> result);
  
              // Set the assignment arrays correctly
              if (fastPassByReferenceIterator.hasNext()) fastPassByReferenceIterator.next();
              else break;
          }
      }
docs: This is an optimization that will fault all the ConcatVectors into memory, and future .get() on the Supplier objs
  will result in a very fast return by reference. Basically this works by wrapping the output of the old thunks
  inside new, thinner closures that carry around the answer in memory. This is a no-op if vectors were already
  cached.