Use arraycopy to create enum values array in enum unboxing

This changes such that a shared utility class now contains (n being the largest enum value of any unboxed enum):

  public static final synthetic $VALUES =
      new int[] { 1, 2, 3, ..., n };

Accesses to MyEnum.$VALUES and calls to MyEnum.values() are rewritten to MyEnum$LocalUtility.values() (m being the largest enum value of MyEnum):

  public static final synthetic values() {
    int[] result = new int[m];
    System.arraycopy($VALUES, 0, result, 0, m);
    return result;
  }

This returns a copy of the underlying $VALUES array when accessing it via a field read. The $VALUES array generally only has a single use, which is followed by a call to clone(). This CL therefore identifies the call to clone() and removes it.

As a result of this, MyEnum.$VALUES == MyEnum.$VALUES will return false (but Arrays.equals(MyEnum$VALUES, MyEnum$VALUES) is true). If this turns out to be a problem, we can consider disabling enum unboxing of enums that have unsupported reads of $VALUES.

Change-Id: I543fb1b73bf34faa12486d00137ca1d0d10cf863
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 14f0015..3d1a246 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -151,6 +151,17 @@
   private StackMapStatus stackMapStatus = StackMapStatus.NOT_VERIFIED;
 
   public CfCode(
+      DexType originalHolder, int maxStack, int maxLocals, List<CfInstruction> instructions) {
+    this(
+        originalHolder,
+        maxStack,
+        maxLocals,
+        instructions,
+        Collections.emptyList(),
+        Collections.emptyList());
+  }
+
+  public CfCode(
       DexType originalHolder,
       int maxStack,
       int maxLocals,
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 0611cdd5..da8323c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -31,6 +31,10 @@
 
 public class DexEncodedField extends DexEncodedMember<DexEncodedField, DexField>
     implements StructuralItem<DexEncodedField> {
+
+  public static final boolean D8_R8_SYNTHESIZED = true;
+  public static final boolean NOT_DEPRECATED = false;
+  public static final DexValue NO_STATIC_VALUE = null;
   public static final DexEncodedField[] EMPTY_ARRAY = {};
 
   public final FieldAccessFlags accessFlags;
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index dedc362..d7a48c1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1511,16 +1511,16 @@
   }
 
   public class JavaLangSystemMethods {
-    public final DexMethod identityHashCode;
 
-    private JavaLangSystemMethods() {
-      identityHashCode =
-          createMethod(
-              javaLangSystemDescriptor,
-              identityHashCodeName,
-              intDescriptor,
-              new DexString[] {objectDescriptor});
-    }
+    public final DexMethod arraycopy =
+        createMethod(
+            javaLangSystemType,
+            createProto(voidType, objectType, intType, objectType, intType, intType),
+            "arraycopy");
+    public final DexMethod identityHashCode =
+        createMethod(javaLangSystemType, createProto(intType, objectType), identityHashCodeName);
+
+    private JavaLangSystemMethods() {}
   }
 
   public class EnumMembers {
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java b/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
index 1697f43..850c3cb 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessFlags.java
@@ -58,6 +58,14 @@
     return this;
   }
 
+  public static FieldAccessFlags createPublicStaticFinalSynthetic() {
+    return fromSharedAccessFlags(
+        Constants.ACC_PUBLIC
+            | Constants.ACC_STATIC
+            | Constants.ACC_FINAL
+            | Constants.ACC_SYNTHETIC);
+  }
+
   public static FieldAccessFlags createPublicStaticSynthetic() {
     return fromSharedAccessFlags(
         Constants.ACC_PUBLIC | Constants.ACC_STATIC | Constants.ACC_SYNTHETIC);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index 21608cf..44896ea 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -73,34 +73,22 @@
       this.defaultCodeOptimizations = defaultCodeOptimizations;
     }
 
-    private void put(
-        ProgramMethodSet methodsToRevisit, Collection<CodeOptimization> codeOptimizations) {
-      if (codeOptimizations.isEmpty()) {
-        // Nothing to conduct.
-        return;
-      }
-      for (ProgramMethod method : methodsToRevisit) {
-        methodsToReprocess.add(method);
-        optimizationsMap
-            .computeIfAbsent(
-                method.getReference(),
-                // Optimization order might matter, hence a collection that preserves orderings.
-                k -> new LinkedHashSet<>())
-            .addAll(codeOptimizations);
-      }
+    public void add(ProgramMethod method) {
+      methodsToReprocess.add(method);
+      optimizationsMap
+          .computeIfAbsent(
+              method.getReference(),
+              // Optimization order might matter, hence a collection that preserves orderings.
+              k -> new LinkedHashSet<>())
+          .addAll(defaultCodeOptimizations);
     }
 
     public void put(ProgramMethodSet methodsToRevisit) {
-      put(methodsToRevisit, defaultCodeOptimizations);
+      methodsToRevisit.forEach(this::add);
     }
 
     public void put(PostOptimization postOptimization) {
-      Collection<CodeOptimization> codeOptimizations =
-          postOptimization.codeOptimizationsForPostProcessing();
-      if (codeOptimizations == null) {
-        codeOptimizations = defaultCodeOptimizations;
-      }
-      put(postOptimization.methodsToRevisit(), codeOptimizations);
+      put(postOptimization.methodsToRevisit());
     }
 
     // Some optimizations may change methods, creating new instances of the encoded methods with a
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostOptimization.java b/src/main/java/com/android/tools/r8/ir/conversion/PostOptimization.java
index 7f06207..30c873d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostOptimization.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import java.util.Collection;
 
 /**
  * An abstraction of optimizations that require post processing of methods.
@@ -13,16 +12,4 @@
 
   /** @return a set of methods that need post processing. */
   ProgramMethodSet methodsToRevisit();
-
-  // TODO(b/127694949): different CodeOptimization for primary processor v.s. post processor?
-  //  In that way, instead of internal state changes, such as COLLECT v.s. APPLY or REVISIT,
-  //  optimizers that need post processing can return what to do at each processor.
-  // Collection<CodeOptimization> codeOptimizationsForPrimaryProcessing();
-
-  /**
-   * @return specific collection of {@link CodeOptimization}s to conduct during post processing.
-   *   Otherwise, i.e., if the default one---IRConverter's full processing---is okay,
-   *   returns {@code null}.
-   */
-  Collection<CodeOptimization> codeOptimizationsForPostProcessing();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
index 3403e7b..2f04f11 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
@@ -27,7 +27,6 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.conversion.CodeOptimization;
 import com.android.tools.r8.ir.conversion.PostOptimization;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
@@ -42,7 +41,6 @@
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Sets;
-import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -490,12 +488,6 @@
     return targetsToRevisit;
   }
 
-  @Override
-  public Collection<CodeOptimization> codeOptimizationsForPostProcessing() {
-    // Run IRConverter#optimize.
-    return null;
-  }
-
   private synchronized boolean verifyAllProgramDispatchTargetsHaveBeenAbandoned(
       InvokeMethod invoke, ProgramMethod context) {
     ProgramMethodSet targets = invoke.lookupProgramDispatchTargets(appView, context);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index a6f246f..9e82361 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -46,7 +46,6 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Throw;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.conversion.CodeOptimization;
 import com.android.tools.r8.ir.conversion.LensCodeRewriter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostOptimization;
@@ -71,7 +70,6 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -244,12 +242,6 @@
     return doubleInlineCallers;
   }
 
-  @Override
-  public Collection<CodeOptimization> codeOptimizationsForPostProcessing() {
-    // Run IRConverter#optimize.
-    return null;  // Technically same as return converter.getOptimizationForPostIRProcessing();
-  }
-
   /**
    * Encodes the constraints for inlining a method's instructions into a different context.
    * <p>
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
index 07471e7..b47ec05 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
 import com.google.common.collect.ImmutableMap;
@@ -30,6 +31,12 @@
     return map.isEmpty();
   }
 
+  public EnumData get(DexProgramClass enumClass) {
+    EnumData enumData = map.get(enumClass.getType());
+    assert enumData != null;
+    return enumData;
+  }
+
   public Set<DexType> getUnboxedEnums() {
     return map.keySet();
   }
@@ -55,6 +62,16 @@
     return map.get(enumType).getValuesSize();
   }
 
+  public int getMaxValuesSize() {
+    int maxValuesSize = 0;
+    for (EnumData data : map.values()) {
+      if (data.hasValues()) {
+        maxValuesSize = Math.max(maxValuesSize, data.getValuesSize());
+      }
+    }
+    return maxValuesSize;
+  }
+
   public boolean matchesValuesField(DexField staticField) {
     assert map.containsKey(staticField.holder);
     return map.get(staticField.holder).matchesValuesField(staticField);
@@ -101,8 +118,12 @@
       return valuesFields.contains(field);
     }
 
+    public boolean hasValues() {
+      return valuesSize != INVALID_VALUES_SIZE;
+    }
+
     public int getValuesSize() {
-      assert valuesSize != INVALID_VALUES_SIZE;
+      assert hasValues();
       return valuesSize;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 7bc7e56..edd774e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -478,11 +478,18 @@
     DirectMappedDexApplication.Builder appBuilder = appView.appInfo().app().asDirect().builder();
     FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder =
         FieldAccessInfoCollectionModifier.builder();
+
     EnumUnboxingUtilityClasses utilityClasses =
         EnumUnboxingUtilityClasses.builder(appView)
             .synthesizeEnumUnboxingUtilityClasses(
-                enumClassesToUnbox, appBuilder, fieldAccessInfoCollectionModifierBuilder)
+                enumClassesToUnbox,
+                enumDataMap,
+                appBuilder,
+                fieldAccessInfoCollectionModifierBuilder)
             .build();
+    utilityClasses.forEach(
+        utilityClass -> utilityClass.getDefinition().forEachProgramMethod(postBuilder::add));
+
     fieldAccessInfoCollectionModifierBuilder.build().modify(appView);
     enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumDataMap, utilityClasses);
     EnumUnboxingLens enumUnboxingLens =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 9b159f2..3bbd636 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -4,10 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.enums;
 
-import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
-
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexAnnotationSet;
@@ -47,12 +44,14 @@
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
 import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.ListIterator;
@@ -70,6 +69,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory factory;
+  private final InternalOptions options;
   private final EnumDataMap unboxedEnumsData;
   private EnumUnboxingLens enumUnboxingLens;
   private final EnumUnboxingUtilityClasses utilityClasses;
@@ -79,7 +79,6 @@
   private final DexMethod ordinalUtilityMethod;
   private final DexMethod equalsUtilityMethod;
   private final DexMethod compareToUtilityMethod;
-  private final DexMethod valuesUtilityMethod;
   private final DexMethod zeroCheckMethod;
   private final DexMethod zeroCheckMessageMethod;
 
@@ -89,12 +88,12 @@
       EnumUnboxingUtilityClasses utilityClasses) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
+    this.options = appView.options();
     this.unboxedEnumsData = unboxedEnumsInstanceFieldData;
     this.utilityClasses = utilityClasses;
 
     // Custom methods for java.lang.Enum methods ordinal, equals and compareTo.
-    DexType sharedEnumUnboxingUtilityType =
-        utilityClasses.getSharedEnumUnboxingUtilityClass().getType();
+    DexType sharedEnumUnboxingUtilityType = utilityClasses.getSharedUtilityClass().getType();
     this.ordinalUtilityMethod =
         factory.createMethod(
             sharedEnumUnboxingUtilityType,
@@ -110,12 +109,6 @@
             sharedEnumUnboxingUtilityType,
             factory.createProto(factory.intType, factory.intType, factory.intType),
             ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "compareTo");
-    // Custom methods for generated field $VALUES initialization.
-    this.valuesUtilityMethod =
-        factory.createMethod(
-            sharedEnumUnboxingUtilityType,
-            factory.createProto(factory.intArrayType, factory.intType),
-            ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "values");
     // Custom methods for Object#getClass without outValue and Objects.requireNonNull.
     this.zeroCheckMethod =
         factory.createMethod(
@@ -129,6 +122,14 @@
             ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "zeroCheckMessage");
   }
 
+  private LocalEnumUnboxingUtilityClass getLocalUtilityClass(DexType enumType) {
+    return utilityClasses.getLocalUtilityClass(enumType);
+  }
+
+  private SharedEnumUnboxingUtilityClass getSharedUtilityClass() {
+    return utilityClasses.getSharedUtilityClass();
+  }
+
   public void setEnumUnboxingLens(EnumUnboxingLens enumUnboxingLens) {
     this.enumUnboxingLens = enumUnboxingLens;
   }
@@ -144,13 +145,21 @@
     Map<Instruction, DexType> convertedEnums = new IdentityHashMap<>();
     Set<Phi> affectedPhis = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blocks = code.listIterator();
+    Set<BasicBlock> seenBlocks = Sets.newIdentityHashSet();
+    Set<Instruction> instructionsToRemove = Sets.newIdentityHashSet();
     Value zeroConstValue = null;
     while (blocks.hasNext()) {
       BasicBlock block = blocks.next();
+      seenBlocks.add(block);
       zeroConstValue = fixNullsInBlockPhis(code, block, zeroConstValue);
       InstructionListIterator iterator = block.listIterator(code);
       while (iterator.hasNext()) {
         Instruction instruction = iterator.next();
+        if (instructionsToRemove.contains(instruction)) {
+          iterator.removeOrReplaceByDebugLocalRead();
+          continue;
+        }
+
         // Rewrites specific enum methods, such as ordinal, into their corresponding enum unboxed
         // counterpart. The rewriting (== or match) is based on the following:
         // - name, ordinal and compareTo are final and implemented only on java.lang.Enum,
@@ -302,38 +311,46 @@
           StaticGet staticGet = instruction.asStaticGet();
           DexField field = staticGet.getField();
           DexType holder = field.holder;
-          if (unboxedEnumsData.isUnboxedEnum(holder)) {
-            if (staticGet.outValue() == null) {
-              iterator.removeOrReplaceByDebugLocalRead();
-              continue;
-            }
-            affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
-            if (unboxedEnumsData.matchesValuesField(field)) {
-              utilityMethods.computeIfAbsent(
-                  valuesUtilityMethod, m -> synthesizeValuesUtilityMethod());
-              DexField fieldValues = createValuesField(holder);
-              DexMethod methodValues = createValuesMethod(holder);
-              utilityMethods.computeIfAbsent(
-                  methodValues,
-                  m ->
-                      computeValuesEncodedMethod(
-                          m, fieldValues, unboxedEnumsData.getValuesSize(holder)));
-              Value rewrittenOutValue =
-                  code.createValue(
-                      ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
-              InvokeStatic invoke =
-                  new InvokeStatic(methodValues, rewrittenOutValue, ImmutableList.of());
-              iterator.replaceCurrentInstruction(invoke);
-              convertedEnums.put(invoke, holder);
-            } else {
-              // Replace by ordinal + 1 for null check (null is 0).
-              assert unboxedEnumsData.hasUnboxedValueFor(field)
-                  : "Invalid read to " + field.name + ", error during enum analysis";
-              ConstNumber intConstant =
-                  code.createIntConstant(unboxedEnumsData.getUnboxedValue(field));
-              iterator.replaceCurrentInstruction(intConstant);
-              convertedEnums.put(intConstant, holder);
-            }
+          if (!unboxedEnumsData.isUnboxedEnum(holder)) {
+            continue;
+          }
+          if (staticGet.hasUnusedOutValue()) {
+            iterator.removeOrReplaceByDebugLocalRead();
+            continue;
+          }
+          affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
+          if (unboxedEnumsData.matchesValuesField(field)) {
+            // Load the size of this enum's $VALUES array before the current instruction.
+            iterator.previous();
+            Value sizeValue =
+                iterator.insertConstIntInstruction(
+                    code, options, unboxedEnumsData.getValuesSize(holder));
+            iterator.next();
+
+            // Replace Enum.$VALUES by a call to: int[] SharedUtilityClass.values(int size).
+            InvokeStatic invoke =
+                InvokeStatic.builder()
+                    .setMethod(getSharedUtilityClass().getValuesMethod())
+                    .setFreshOutValue(appView, code)
+                    .setSingleArgument(sizeValue)
+                    .build();
+            iterator.replaceCurrentInstruction(invoke);
+
+            convertedEnums.put(invoke, holder);
+
+            // Check if the call to SharedUtilityClass.values(size) is followed by a call to
+            // clone(). If so, remove it, since SharedUtilityClass.values(size) returns a fresh
+            // array. This is needed because the javac generated implementation of MyEnum.values()
+            // is implemented as `return $VALUES.clone()`.
+            removeRedundantValuesArrayCloning(invoke, instructionsToRemove, seenBlocks);
+          } else {
+            // Replace by ordinal + 1 for null check (null is 0).
+            assert unboxedEnumsData.hasUnboxedValueFor(field)
+                : "Invalid read to " + field.name + ", error during enum analysis";
+            ConstNumber intConstant =
+                code.createIntConstant(unboxedEnumsData.getUnboxedValue(field));
+            iterator.replaceCurrentInstruction(intConstant);
+            convertedEnums.put(intConstant, holder);
           }
         }
 
@@ -376,6 +393,26 @@
     return affectedPhis;
   }
 
+  private void removeRedundantValuesArrayCloning(
+      InvokeStatic invoke, Set<Instruction> instructionsToRemove, Set<BasicBlock> seenBlocks) {
+    for (Instruction user : invoke.outValue().aliasedUsers()) {
+      if (user.isInvokeVirtual()) {
+        InvokeVirtual cloneCandidate = user.asInvokeVirtual();
+        if (cloneCandidate.getInvokedMethod().match(appView.dexItemFactory().objectMembers.clone)) {
+          if (cloneCandidate.hasOutValue()) {
+            cloneCandidate.outValue().replaceUsers(invoke.outValue());
+          }
+          BasicBlock cloneBlock = cloneCandidate.getBlock();
+          if (cloneBlock == invoke.getBlock() || !seenBlocks.contains(cloneBlock)) {
+            instructionsToRemove.add(cloneCandidate);
+          } else {
+            cloneBlock.removeInstruction(cloneCandidate);
+          }
+        }
+      }
+    }
+  }
+
   private void rewriteNameMethod(
       InstructionListIterator iterator, InvokeMethodWithReceiver invokeMethod, DexType enumType) {
     DexMethod toStringMethod =
@@ -406,7 +443,7 @@
     while (iterator.hasNext() && iterator.peekNext().isArgument()) {
       iterator.next();
     }
-    return iterator.insertConstNumberInstruction(code, appView.options(), 0, TypeElement.getInt());
+    return iterator.insertConstIntInstruction(code, options, 0);
   }
 
   private DexMethod computeInstanceFieldMethod(DexField field) {
@@ -465,35 +502,6 @@
     return type.toSourceString().replace('.', '$');
   }
 
-  private DexField createValuesField(DexType enumType) {
-    return createValuesField(
-        enumType, utilityClasses.getLocalEnumUnboxingUtilityClass(enumType), factory);
-  }
-
-  static DexField createValuesField(
-      DexType enumType, DexType enumUtilityClass, DexItemFactory dexItemFactory) {
-    return dexItemFactory.createField(
-        enumUtilityClass,
-        dexItemFactory.intArrayType,
-        "$$values$$field$" + compatibleName(enumType));
-  }
-
-  private DexMethod createValuesMethod(DexType enumType) {
-    return factory.createMethod(
-        utilityClasses.getLocalEnumUnboxingUtilityClass(enumType),
-        factory.createProto(factory.intArrayType),
-        "$$values$$method$" + compatibleName(enumType));
-  }
-
-  private DexEncodedMethod computeValuesEncodedMethod(
-      DexMethod method, DexField fieldValues, int numEnumInstances) {
-    CfCode cfCode =
-        new EnumUnboxingCfCodeProvider.EnumUnboxingValuesCfCodeProvider(
-                appView, method.holder, fieldValues, numEnumInstances, valuesUtilityMethod)
-            .generateCfCode();
-    return synthesizeUtilityMethod(cfCode, method, true);
-  }
-
   private DexMethod computeInstanceFieldUtilityMethod(DexType enumType, DexField field) {
     assert unboxedEnumsData.isUnboxedEnum(enumType);
     assert field.holder == enumType || field.holder == factory.enumType;
@@ -505,7 +513,7 @@
             + compatibleName(enumType);
     DexMethod fieldMethod =
         factory.createMethod(
-            utilityClasses.getLocalEnumUnboxingUtilityClass(enumType),
+            utilityClasses.getLocalUtilityClass(enumType).getType(),
             factory.createProto(field.type, factory.intType),
             methodName);
     utilityMethods.computeIfAbsent(
@@ -519,7 +527,7 @@
     String methodName = "string$valueOf$" + compatibleName(enumType);
     DexMethod fieldMethod =
         factory.createMethod(
-            utilityClasses.getLocalEnumUnboxingUtilityClass(enumType),
+            utilityClasses.getLocalUtilityClass(enumType).getType(),
             factory.createProto(factory.stringType, factory.intType),
             methodName);
     AbstractValue nullString =
@@ -534,7 +542,7 @@
     assert unboxedEnumsData.isUnboxedEnum(enumType);
     DexMethod valueOf =
         factory.createMethod(
-            utilityClasses.getLocalEnumUnboxingUtilityClass(enumType),
+            utilityClasses.getLocalUtilityClass(enumType).getType(),
             factory.createProto(factory.intType, factory.stringType),
             "valueOf" + compatibleName(enumType));
     utilityMethods.computeIfAbsent(valueOf, m -> synthesizeValueOfUtilityMethod(m, enumType));
@@ -594,7 +602,7 @@
     }
     // We make the order deterministic.
     for (List<T> value : encodedMembersMap.values()) {
-      value.sort((m1, m2) -> m1.getReference().compareTo(m2.getReference()));
+      value.sort(Comparator.comparing(DexEncodedMember::getReference));
     }
     return encodedMembersMap;
   }
@@ -611,7 +619,7 @@
                 unboxedEnumsData.getInstanceFieldData(enumType, field).asEnumFieldMappingData(),
                 nullValue)
             .generateCfCode();
-    return synthesizeUtilityMethod(cfCode, method, false);
+    return synthesizeUtilityMethod(cfCode, method);
   }
 
   private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
@@ -628,51 +636,45 @@
                     .getInstanceFieldData(enumType, factory.enumMembers.nameField)
                     .asEnumFieldMappingData())
             .generateCfCode();
-    return synthesizeUtilityMethod(cfCode, method, false);
+    return synthesizeUtilityMethod(cfCode, method);
   }
 
   private DexEncodedMethod synthesizeZeroCheckMethod() {
     CfCode cfCode =
         EnumUnboxingCfMethods.EnumUnboxingMethods_zeroCheck(appView.options(), zeroCheckMethod);
-    return synthesizeUtilityMethod(cfCode, zeroCheckMethod, false);
+    return synthesizeUtilityMethod(cfCode, zeroCheckMethod);
   }
 
   private DexEncodedMethod synthesizeZeroCheckMessageMethod() {
     CfCode cfCode =
         EnumUnboxingCfMethods.EnumUnboxingMethods_zeroCheckMessage(
             appView.options(), zeroCheckMessageMethod);
-    return synthesizeUtilityMethod(cfCode, zeroCheckMessageMethod, false);
+    return synthesizeUtilityMethod(cfCode, zeroCheckMessageMethod);
   }
 
   private DexEncodedMethod synthesizeOrdinalMethod() {
     CfCode cfCode =
         EnumUnboxingCfMethods.EnumUnboxingMethods_ordinal(appView.options(), ordinalUtilityMethod);
-    return synthesizeUtilityMethod(cfCode, ordinalUtilityMethod, false);
+    return synthesizeUtilityMethod(cfCode, ordinalUtilityMethod);
   }
 
   private DexEncodedMethod synthesizeEqualsMethod() {
     CfCode cfCode =
         EnumUnboxingCfMethods.EnumUnboxingMethods_equals(appView.options(), equalsUtilityMethod);
-    return synthesizeUtilityMethod(cfCode, equalsUtilityMethod, false);
+    return synthesizeUtilityMethod(cfCode, equalsUtilityMethod);
   }
 
   private DexEncodedMethod synthesizeCompareToMethod() {
     CfCode cfCode =
         EnumUnboxingCfMethods.EnumUnboxingMethods_compareTo(
             appView.options(), compareToUtilityMethod);
-    return synthesizeUtilityMethod(cfCode, compareToUtilityMethod, false);
+    return synthesizeUtilityMethod(cfCode, compareToUtilityMethod);
   }
 
-  private DexEncodedMethod synthesizeValuesUtilityMethod() {
-    CfCode cfCode =
-        EnumUnboxingCfMethods.EnumUnboxingMethods_values(appView.options(), valuesUtilityMethod);
-    return synthesizeUtilityMethod(cfCode, valuesUtilityMethod, false);
-  }
-
-  private DexEncodedMethod synthesizeUtilityMethod(CfCode cfCode, DexMethod method, boolean sync) {
+  private DexEncodedMethod synthesizeUtilityMethod(CfCode cfCode, DexMethod method) {
     return new DexEncodedMethod(
         method,
-        synthesizedMethodAccessFlags(sync),
+        MethodAccessFlags.createPublicStaticSynthetic(),
         MethodTypeSignature.noSignature(),
         DexAnnotationSet.empty(),
         ParameterAnnotationsList.empty(),
@@ -680,12 +682,4 @@
         true,
         REQUIRED_CLASS_FILE_VERSION);
   }
-
-  private MethodAccessFlags synthesizedMethodAccessFlags(boolean sync) {
-    int access = Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC | Constants.ACC_STATIC;
-    if (sync) {
-      access = access | Constants.ACC_SYNCHRONIZED;
-    }
-    return MethodAccessFlags.fromSharedAccessFlags(access, false);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index d36d9b7..8bab979 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -67,7 +67,7 @@
                   if (m.isInitializer()) {
                     clearEnumToUnboxMethod(m);
                   } else {
-                    DexType newHolder = utilityClasses.getLocalEnumUnboxingUtilityClass(clazz);
+                    DexType newHolder = utilityClasses.getLocalUtilityClass(clazz).getType();
                     List<DexEncodedMethod> movedMethods =
                         unboxedEnumsMethods.computeIfAbsent(newHolder, k -> new ArrayList<>());
                     movedMethods.add(fixupEncodedMethodToUtility(m, newHolder));
@@ -178,7 +178,7 @@
       newMethod =
           factory.createInstanceInitializerWithFreshProto(
               newMethod,
-              utilityClasses.getSharedEnumUnboxingUtilityClass().getType(),
+              utilityClasses.getSharedUtilityClass().getType(),
               tryMethod -> holder.lookupMethod(tryMethod) == null);
     } else {
       int index = 0;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClass.java
new file mode 100644
index 0000000..6026c71
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClass.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.graph.DexProgramClass;
+
+public abstract class EnumUnboxingUtilityClass {
+
+  public abstract DexProgramClass getDefinition();
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClasses.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClasses.java
index dda4779..1753375 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClasses.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingUtilityClasses.java
@@ -4,63 +4,51 @@
 
 package com.android.tools.r8.ir.optimize.enums;
 
-import static com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter.createValuesField;
-
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.ClassAccessFlags;
-import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
-import com.android.tools.r8.graph.FieldAccessFlags;
-import com.android.tools.r8.graph.GenericSignature.ClassSignature;
-import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.google.common.collect.ImmutableMap;
-import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 
 public class EnumUnboxingUtilityClasses {
 
-  public static final String ENUM_UNBOXING_LOCAL_UTILITY_CLASS_SUFFIX =
-      "$r8$EnumUnboxingLocalUtility";
-  public static final String ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX =
-      "$r8$EnumUnboxingSharedUtility";
-
   // Synthetic classes for utilities specific to the unboxing of a single enum.
-  private final ImmutableMap<DexType, DexProgramClass> localEnumUnboxingUtilityClasses;
+  private final ImmutableMap<DexType, LocalEnumUnboxingUtilityClass> localUtilityClasses;
 
   // Default enum unboxing utility synthetic class used to hold all the shared unboxed enum
   // methods (ordinal(I), equals(II), etc.).
-  private final DexProgramClass sharedEnumUnboxingUtilityClass;
+  private final SharedEnumUnboxingUtilityClass sharedUtilityClass;
 
   private EnumUnboxingUtilityClasses(
-      DexProgramClass sharedEnumUnboxingUtilityClass,
-      ImmutableMap<DexType, DexProgramClass> localEnumUnboxingUtilityClasses) {
-    this.sharedEnumUnboxingUtilityClass = sharedEnumUnboxingUtilityClass;
-    this.localEnumUnboxingUtilityClasses = localEnumUnboxingUtilityClasses;
+      SharedEnumUnboxingUtilityClass sharedUtilityClass,
+      ImmutableMap<DexType, LocalEnumUnboxingUtilityClass> localUtilityClasses) {
+    this.sharedUtilityClass = sharedUtilityClass;
+    this.localUtilityClasses = localUtilityClasses;
   }
 
-  public DexType getLocalEnumUnboxingUtilityClass(DexProgramClass enumClass) {
-    return getLocalEnumUnboxingUtilityClass(enumClass.getType());
+  public void forEach(Consumer<? super EnumUnboxingUtilityClass> consumer) {
+    localUtilityClasses.values().forEach(consumer);
+    consumer.accept(getSharedUtilityClass());
   }
 
-  public DexType getLocalEnumUnboxingUtilityClass(DexType enumType) {
-    DexProgramClass localEnumUnboxingUtilityClass = localEnumUnboxingUtilityClasses.get(enumType);
+  public LocalEnumUnboxingUtilityClass getLocalUtilityClass(DexProgramClass enumClass) {
+    return getLocalUtilityClass(enumClass.getType());
+  }
+
+  public LocalEnumUnboxingUtilityClass getLocalUtilityClass(DexType enumType) {
+    LocalEnumUnboxingUtilityClass localEnumUnboxingUtilityClass = localUtilityClasses.get(enumType);
     assert localEnumUnboxingUtilityClass != null;
-    return localEnumUnboxingUtilityClass.getType();
+    return localEnumUnboxingUtilityClass;
   }
 
-  public DexProgramClass getSharedEnumUnboxingUtilityClass() {
-    return sharedEnumUnboxingUtilityClass;
+  public SharedEnumUnboxingUtilityClass getSharedUtilityClass() {
+    return sharedUtilityClass;
   }
 
   public static Builder builder(AppView<AppInfoWithLiveness> appView) {
@@ -69,10 +57,9 @@
 
   public static class Builder {
 
-    private final AppView<?> appView;
-    private final Map<DexType, DexProgramClass> localEnumUnboxingUtilityClasses =
-        new IdentityHashMap<>();
-    private DexProgramClass sharedEnumUnboxingUtilityClass;
+    private final AppView<AppInfoWithLiveness> appView;
+    private ImmutableMap<DexType, LocalEnumUnboxingUtilityClass> localUtilityClasses;
+    private SharedEnumUnboxingUtilityClass sharedUtilityClass;
 
     public Builder(AppView<AppInfoWithLiveness> appView) {
       this.appView = appView;
@@ -80,130 +67,42 @@
 
     public Builder synthesizeEnumUnboxingUtilityClasses(
         Set<DexProgramClass> enumsToUnbox,
+        EnumDataMap enumDataMap,
         DirectMappedDexApplication.Builder appBuilder,
         FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder) {
-      synthesizeLocalUtilityClasses(
-          enumsToUnbox, appBuilder, fieldAccessInfoCollectionModifierBuilder);
-      synthesizeSharedUtilityClass(enumsToUnbox, appBuilder);
+      SharedEnumUnboxingUtilityClass sharedUtilityClass =
+          SharedEnumUnboxingUtilityClass.builder(
+                  appView, enumDataMap, enumsToUnbox, fieldAccessInfoCollectionModifierBuilder)
+              .build(appBuilder);
+      ImmutableMap<DexType, LocalEnumUnboxingUtilityClass> localUtilityClasses =
+          createLocalUtilityClasses(enumsToUnbox, appBuilder);
+      this.localUtilityClasses = localUtilityClasses;
+      this.sharedUtilityClass = sharedUtilityClass;
       return this;
     }
 
     public EnumUnboxingUtilityClasses build() {
-      return new EnumUnboxingUtilityClasses(
-          sharedEnumUnboxingUtilityClass, ImmutableMap.copyOf(localEnumUnboxingUtilityClasses));
+      return new EnumUnboxingUtilityClasses(sharedUtilityClass, localUtilityClasses);
     }
 
-    private void synthesizeLocalUtilityClasses(
-        Set<DexProgramClass> enumsToUnbox,
-        DirectMappedDexApplication.Builder appBuilder,
-        FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder) {
-      for (DexProgramClass enumToUnbox : enumsToUnbox) {
-        synthesizeLocalUtilityClass(
-            enumToUnbox, appBuilder, fieldAccessInfoCollectionModifierBuilder);
-      }
-    }
-
-    private void synthesizeLocalUtilityClass(
-        DexProgramClass enumToUnbox,
-        DirectMappedDexApplication.Builder appBuilder,
-        FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder) {
-      DexType localUtilityClassType = getLocalUtilityClassType(enumToUnbox);
-      assert appView.appInfo().definitionForWithoutExistenceAssert(localUtilityClassType) == null;
-
-      // Required fields.
-      DexField reference =
-          createValuesField(enumToUnbox.getType(), localUtilityClassType, appView.dexItemFactory());
-      DexEncodedField staticField =
-          new DexEncodedField(reference, FieldAccessFlags.createPublicStaticSynthetic());
-      fieldAccessInfoCollectionModifierBuilder
-          .recordFieldReadInUnknownContext(reference)
-          .recordFieldWriteInUnknownContext(reference);
-
-      DexProgramClass localUtilityClass =
-          new DexProgramClass(
-              localUtilityClassType,
-              null,
-              new SynthesizedOrigin("enum unboxing", EnumUnboxer.class),
-              ClassAccessFlags.createPublicFinalSynthetic(),
-              appView.dexItemFactory().objectType,
-              DexTypeList.empty(),
-              null,
-              null,
-              Collections.emptyList(),
-              null,
-              Collections.emptyList(),
-              ClassSignature.noSignature(),
-              DexAnnotationSet.empty(),
-              new DexEncodedField[] {staticField},
-              DexEncodedField.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              appView.dexItemFactory().getSkipNameValidationForTesting(),
-              DexProgramClass::checksumFromType);
-      appBuilder.addSynthesizedClass(localUtilityClass);
-      appView.appInfo().addSynthesizedClass(localUtilityClass, enumToUnbox);
-      localEnumUnboxingUtilityClasses.put(enumToUnbox.getType(), localUtilityClass);
-    }
-
-    private void synthesizeSharedUtilityClass(
+    private ImmutableMap<DexType, LocalEnumUnboxingUtilityClass> createLocalUtilityClasses(
         Set<DexProgramClass> enumsToUnbox, DirectMappedDexApplication.Builder appBuilder) {
-      DexType type = getSharedUtilityClassType(findDeterministicContextType(enumsToUnbox));
-      assert appView.appInfo().definitionForWithoutExistenceAssert(type) == null;
-
-      DexProgramClass syntheticClass =
-          new DexProgramClass(
-              type,
-              null,
-              new SynthesizedOrigin("enum unboxing", EnumUnboxer.class),
-              ClassAccessFlags.createPublicFinalSynthetic(),
-              appView.dexItemFactory().objectType,
-              DexTypeList.empty(),
-              null,
-              null,
-              Collections.emptyList(),
-              null,
-              Collections.emptyList(),
-              ClassSignature.noSignature(),
-              DexAnnotationSet.empty(),
-              DexEncodedField.EMPTY_ARRAY,
-              DexEncodedField.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              DexEncodedMethod.EMPTY_ARRAY,
-              appView.dexItemFactory().getSkipNameValidationForTesting(),
-              DexProgramClass::checksumFromType);
-      appBuilder.addSynthesizedClass(syntheticClass);
-      appView.appInfo().addSynthesizedClassToBase(syntheticClass, enumsToUnbox);
-      sharedEnumUnboxingUtilityClass = syntheticClass;
-    }
-
-    private DexProgramClass findDeterministicContextType(Set<DexProgramClass> contexts) {
-      DexProgramClass deterministicContext = null;
-      for (DexProgramClass context : contexts) {
-        if (deterministicContext == null) {
-          deterministicContext = context;
-        } else if (context.type.compareTo(deterministicContext.type) < 0) {
-          deterministicContext = context;
-        }
+      ImmutableMap.Builder<DexType, LocalEnumUnboxingUtilityClass> localUtilityClasses =
+          ImmutableMap.builder();
+      for (DexProgramClass enumToUnbox : enumsToUnbox) {
+        localUtilityClasses.put(
+            enumToUnbox.getType(),
+            LocalEnumUnboxingUtilityClass.builder(appView, enumToUnbox).build(appBuilder));
       }
-      return deterministicContext;
+      return localUtilityClasses.build();
     }
 
-    private DexType getLocalUtilityClassType(DexProgramClass context) {
-      return getUtilityClassType(context, ENUM_UNBOXING_LOCAL_UTILITY_CLASS_SUFFIX);
-    }
-
-    private DexType getSharedUtilityClassType(DexProgramClass context) {
-      return getUtilityClassType(context, ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX);
-    }
-
-    private DexType getUtilityClassType(DexProgramClass context, String suffix) {
-      return appView
-          .dexItemFactory()
-          .createType(
-              DescriptorUtils.getDescriptorFromClassBinaryName(
-                  DescriptorUtils.getBinaryNameFromDescriptor(
-                          context.getType().toDescriptorString())
-                      + suffix));
+    static DexType getUtilityClassType(
+        DexProgramClass context, String suffix, DexItemFactory dexItemFactory) {
+      return dexItemFactory.createType(
+          DescriptorUtils.getDescriptorFromClassBinaryName(
+              DescriptorUtils.getBinaryNameFromDescriptor(context.getType().toDescriptorString())
+                  + suffix));
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
new file mode 100644
index 0000000..f4d4636
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/LocalEnumUnboxingUtilityClass.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Collections;
+
+public class LocalEnumUnboxingUtilityClass extends EnumUnboxingUtilityClass {
+
+  private static final String ENUM_UNBOXING_LOCAL_UTILITY_CLASS_SUFFIX =
+      "$r8$EnumUnboxingLocalUtility";
+
+  private final DexProgramClass localUtilityClass;
+
+  public LocalEnumUnboxingUtilityClass(DexProgramClass localUtilityClass) {
+    this.localUtilityClass = localUtilityClass;
+  }
+
+  public static Builder builder(AppView<AppInfoWithLiveness> appView, DexProgramClass enumToUnbox) {
+    return new Builder(appView, enumToUnbox);
+  }
+
+  @Override
+  public DexProgramClass getDefinition() {
+    return localUtilityClass;
+  }
+
+  public DexType getType() {
+    return localUtilityClass.getType();
+  }
+
+  public static class Builder {
+
+    private final AppView<AppInfoWithLiveness> appView;
+    private final DexItemFactory dexItemFactory;
+    private final DexProgramClass enumToUnbox;
+    private final DexType localUtilityClassType;
+
+    private Builder(AppView<AppInfoWithLiveness> appView, DexProgramClass enumToUnbox) {
+      this.appView = appView;
+      this.dexItemFactory = appView.dexItemFactory();
+      this.enumToUnbox = enumToUnbox;
+      this.localUtilityClassType =
+          EnumUnboxingUtilityClasses.Builder.getUtilityClassType(
+              enumToUnbox, ENUM_UNBOXING_LOCAL_UTILITY_CLASS_SUFFIX, dexItemFactory);
+
+      assert appView.appInfo().definitionForWithoutExistenceAssert(localUtilityClassType) == null;
+    }
+
+    LocalEnumUnboxingUtilityClass build(DirectMappedDexApplication.Builder appBuilder) {
+      DexProgramClass clazz = createClass();
+      appBuilder.addSynthesizedClass(clazz);
+      appView.appInfo().addSynthesizedClass(clazz, enumToUnbox);
+      return new LocalEnumUnboxingUtilityClass(clazz);
+    }
+
+    private DexProgramClass createClass() {
+      return new DexProgramClass(
+          localUtilityClassType,
+          null,
+          new SynthesizedOrigin("enum unboxing", EnumUnboxer.class),
+          ClassAccessFlags.createPublicFinalSynthetic(),
+          appView.dexItemFactory().objectType,
+          DexTypeList.empty(),
+          null,
+          null,
+          Collections.emptyList(),
+          null,
+          Collections.emptyList(),
+          ClassSignature.noSignature(),
+          DexAnnotationSet.empty(),
+          DexEncodedField.EMPTY_ARRAY,
+          DexEncodedField.EMPTY_ARRAY,
+          DexEncodedMethod.EMPTY_ARRAY,
+          DexEncodedMethod.EMPTY_ARRAY,
+          appView.dexItemFactory().getSkipNameValidationForTesting(),
+          DexProgramClass::checksumFromType);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
new file mode 100644
index 0000000..19adc9f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
@@ -0,0 +1,279 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.cf.code.CfArrayStore;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNewArray;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.origin.SynthesizedOrigin;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.objectweb.asm.Opcodes;
+
+public class SharedEnumUnboxingUtilityClass extends EnumUnboxingUtilityClass {
+
+  public static final String ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX =
+      "$r8$EnumUnboxingSharedUtility";
+
+  private final DexProgramClass sharedUtilityClass;
+  private final ProgramField valuesField;
+  private final ProgramMethod valuesMethod;
+
+  public SharedEnumUnboxingUtilityClass(
+      DexProgramClass sharedUtilityClass, ProgramField valuesField, ProgramMethod valuesMethod) {
+    this.sharedUtilityClass = sharedUtilityClass;
+    this.valuesField = valuesField;
+    this.valuesMethod = valuesMethod;
+  }
+
+  public static Builder builder(
+      AppView<AppInfoWithLiveness> appView,
+      EnumDataMap enumDataMap,
+      Set<DexProgramClass> enumsToUnbox,
+      FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder) {
+    return new Builder(
+        appView, enumDataMap, enumsToUnbox, fieldAccessInfoCollectionModifierBuilder);
+  }
+
+  @Override
+  public DexProgramClass getDefinition() {
+    return sharedUtilityClass;
+  }
+
+  public ProgramField getValuesField() {
+    return valuesField;
+  }
+
+  public ProgramMethod getValuesMethod() {
+    return valuesMethod;
+  }
+
+  public DexType getType() {
+    return sharedUtilityClass.getType();
+  }
+
+  public static class Builder {
+
+    private final AppView<AppInfoWithLiveness> appView;
+    private final DexItemFactory dexItemFactory;
+    private final EnumDataMap enumDataMap;
+    private final Set<DexProgramClass> enumsToUnbox;
+    private final FieldAccessInfoCollectionModifier.Builder
+        fieldAccessInfoCollectionModifierBuilder;
+    private final DexType sharedUtilityClassType;
+
+    private DexEncodedField valuesField;
+    private DexEncodedMethod valuesMethod;
+
+    private Builder(
+        AppView<AppInfoWithLiveness> appView,
+        EnumDataMap enumDataMap,
+        Set<DexProgramClass> enumsToUnbox,
+        FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder) {
+      this.appView = appView;
+      this.dexItemFactory = appView.dexItemFactory();
+      this.enumDataMap = enumDataMap;
+      this.enumsToUnbox = enumsToUnbox;
+      this.fieldAccessInfoCollectionModifierBuilder = fieldAccessInfoCollectionModifierBuilder;
+      this.sharedUtilityClassType =
+          EnumUnboxingUtilityClasses.Builder.getUtilityClassType(
+              findDeterministicContextType(enumsToUnbox),
+              ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX,
+              dexItemFactory);
+
+      assert appView.appInfo().definitionForWithoutExistenceAssert(sharedUtilityClassType) == null;
+    }
+
+    SharedEnumUnboxingUtilityClass build(DirectMappedDexApplication.Builder appBuilder) {
+      DexProgramClass clazz = createClass();
+      appBuilder.addSynthesizedClass(clazz);
+      appView.appInfo().addSynthesizedClassToBase(clazz, enumsToUnbox);
+      return new SharedEnumUnboxingUtilityClass(
+          clazz, new ProgramField(clazz, valuesField), new ProgramMethod(clazz, valuesMethod));
+    }
+
+    private DexProgramClass createClass() {
+      DexEncodedField valuesField = createValuesField(sharedUtilityClassType);
+      return new DexProgramClass(
+          sharedUtilityClassType,
+          null,
+          new SynthesizedOrigin("enum unboxing", EnumUnboxer.class),
+          ClassAccessFlags.createPublicFinalSynthetic(),
+          dexItemFactory.objectType,
+          DexTypeList.empty(),
+          null,
+          null,
+          Collections.emptyList(),
+          null,
+          Collections.emptyList(),
+          ClassSignature.noSignature(),
+          DexAnnotationSet.empty(),
+          new DexEncodedField[] {valuesField},
+          DexEncodedField.EMPTY_ARRAY,
+          new DexEncodedMethod[] {
+            createClassInitializer(valuesField), createValuesMethod(valuesField)
+          },
+          DexEncodedMethod.EMPTY_ARRAY,
+          dexItemFactory.getSkipNameValidationForTesting(),
+          DexProgramClass::checksumFromType);
+    }
+
+    // Fields.
+
+    private DexEncodedField createValuesField(DexType sharedUtilityClassType) {
+      DexEncodedField valuesField =
+          new DexEncodedField(
+              dexItemFactory.createField(
+                  sharedUtilityClassType, dexItemFactory.intArrayType, "$VALUES"),
+              FieldAccessFlags.createPublicStaticFinalSynthetic(),
+              FieldTypeSignature.noSignature(),
+              DexAnnotationSet.empty(),
+              DexEncodedField.NO_STATIC_VALUE,
+              DexEncodedField.NOT_DEPRECATED,
+              DexEncodedField.D8_R8_SYNTHESIZED);
+      fieldAccessInfoCollectionModifierBuilder
+          .recordFieldReadInUnknownContext(valuesField.getReference())
+          .recordFieldWriteInUnknownContext(valuesField.getReference());
+      this.valuesField = valuesField;
+      return valuesField;
+    }
+
+    // Methods.
+
+    private DexEncodedMethod createClassInitializer(DexEncodedField valuesField) {
+      return new DexEncodedMethod(
+          dexItemFactory.createClassInitializer(sharedUtilityClassType),
+          MethodAccessFlags.createForClassInitializer(),
+          MethodTypeSignature.noSignature(),
+          DexAnnotationSet.empty(),
+          ParameterAnnotationsList.empty(),
+          createClassInitializerCode(valuesField),
+          DexEncodedMethod.D8_R8_SYNTHESIZED,
+          CfVersion.V1_6);
+    }
+
+    private CfCode createClassInitializerCode(DexEncodedField valuesField) {
+      int maxValuesArraySize = enumDataMap.getMaxValuesSize();
+      int numberOfInstructions = 4 + maxValuesArraySize * 4;
+      List<CfInstruction> instructions = new ArrayList<>(numberOfInstructions);
+      instructions.add(new CfConstNumber(maxValuesArraySize, ValueType.INT));
+      instructions.add(new CfNewArray(dexItemFactory.intArrayType));
+      for (int i = 0; i < maxValuesArraySize; i++) {
+        instructions.add(new CfStackInstruction(Opcode.Dup));
+        instructions.add(new CfConstNumber(i, ValueType.INT));
+        // i + 1 because 0 represents the null value.
+        instructions.add(new CfConstNumber(i + 1, ValueType.INT));
+        instructions.add(new CfArrayStore(MemberType.INT));
+      }
+      instructions.add(new CfFieldInstruction(Opcodes.PUTSTATIC, valuesField.getReference()));
+      instructions.add(new CfReturnVoid());
+
+      int maxStack = 4;
+      int maxLocals = 0;
+      return new CfCode(
+          sharedUtilityClassType,
+          maxStack,
+          maxLocals,
+          instructions,
+          Collections.emptyList(),
+          Collections.emptyList());
+    }
+
+    private DexEncodedMethod createValuesMethod(DexEncodedField valuesField) {
+      DexEncodedMethod valuesMethod =
+          new DexEncodedMethod(
+              dexItemFactory.createMethod(
+                  sharedUtilityClassType,
+                  dexItemFactory.createProto(dexItemFactory.intArrayType, dexItemFactory.intType),
+                  "values"),
+              MethodAccessFlags.createPublicStaticSynthetic(),
+              MethodTypeSignature.noSignature(),
+              DexAnnotationSet.empty(),
+              ParameterAnnotationsList.empty(),
+              createValuesMethodCode(valuesField),
+              DexEncodedMethod.D8_R8_SYNTHESIZED,
+              CfVersion.V1_6);
+      this.valuesMethod = valuesMethod;
+      return valuesMethod;
+    }
+
+    private CfCode createValuesMethodCode(DexEncodedField valuesField) {
+      int maxStack = 5;
+      int maxLocals = 2;
+      int argumentLocalSlot = 0;
+      int resultLocalSlot = 1;
+      return new CfCode(
+          sharedUtilityClassType,
+          maxStack,
+          maxLocals,
+          ImmutableList.of(
+              // int[] result = new int[size];
+              new CfLoad(ValueType.INT, argumentLocalSlot),
+              new CfNewArray(dexItemFactory.intArrayType),
+              new CfStore(ValueType.OBJECT, resultLocalSlot),
+              // System.arraycopy(SharedUtilityClass.$VALUES, 0, result, 0, size);
+              new CfFieldInstruction(Opcodes.GETSTATIC, valuesField.getReference()),
+              new CfConstNumber(0, ValueType.INT),
+              new CfLoad(ValueType.OBJECT, resultLocalSlot),
+              new CfConstNumber(0, ValueType.INT),
+              new CfLoad(ValueType.INT, argumentLocalSlot),
+              new CfInvoke(
+                  Opcodes.INVOKESTATIC, dexItemFactory.javaLangSystemMethods.arraycopy, false),
+              // return result
+              new CfLoad(ValueType.OBJECT, resultLocalSlot),
+              new CfReturn(ValueType.OBJECT)),
+          Collections.emptyList(),
+          Collections.emptyList());
+    }
+
+    private static DexProgramClass findDeterministicContextType(Set<DexProgramClass> contexts) {
+      DexProgramClass deterministicContext = null;
+      for (DexProgramClass context : contexts) {
+        if (deterministicContext == null) {
+          deterministicContext = context;
+        } else if (context.type.compareTo(deterministicContext.type) < 0) {
+          deterministicContext = context;
+        }
+      }
+      return deterministicContext;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
index faf1061..e7b5dce 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingEnumUnboxingTest.java
@@ -9,7 +9,7 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.enumunboxing.examplelib1.JavaLibrary1;
-import com.android.tools.r8.ir.optimize.enums.EnumUnboxingUtilityClasses;
+import com.android.tools.r8.ir.optimize.enums.SharedEnumUnboxingUtilityClass;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -90,7 +90,8 @@
                 c ->
                     c.getOriginalName()
                         .contains(
-                            EnumUnboxingUtilityClasses.ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX)));
+                            SharedEnumUnboxingUtilityClass
+                                .ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX)));
   }
 
   static class App {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
index 58d220f..4ef092d 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/DoubleProcessingMergeEnumUnboxingTest.java
@@ -10,7 +10,7 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.enumunboxing.examplelib1.JavaLibrary1;
 import com.android.tools.r8.enumunboxing.examplelib2.JavaLibrary2;
-import com.android.tools.r8.ir.optimize.enums.EnumUnboxingUtilityClasses;
+import com.android.tools.r8.ir.optimize.enums.SharedEnumUnboxingUtilityClass;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -95,7 +95,8 @@
                 c ->
                     c.getOriginalName()
                         .contains(
-                            EnumUnboxingUtilityClasses.ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX)));
+                            SharedEnumUnboxingUtilityClass
+                                .ENUM_UNBOXING_SHARED_UTILITY_CLASS_SUFFIX)));
   }
 
   static class App {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/RedundantValuesCloneEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/RedundantValuesCloneEnumUnboxingTest.java
new file mode 100644
index 0000000..82c48eb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/RedundantValuesCloneEnumUnboxingTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.enumunboxing;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RedundantValuesCloneEnumUnboxingTest extends TestBase {
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        // Verify that there are no calls to clone().
+        .inspect(
+            inspector ->
+                inspector.forAllClasses(
+                    clazz ->
+                        clazz.forAllMethods(
+                            method -> assertThat(method, not(invokesMethodWithName("clone"))))))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      for (MyEnum e : MyEnum.values()) {
+        System.out.println(e.name());
+      }
+    }
+  }
+
+  enum MyEnum {
+    A
+  }
+}