What
Three widget entry points call Enum.GetNames / Enum.GetValues inside the draw path, so every enum combo allocates two fresh arrays per frame, per row, for the life of the application. In an immediate-mode library this is the hot path by definition — a settings panel with a handful of enum rows at 60 fps allocates thousands of arrays a second, all immediately garbage.
| Site |
Calls |
ImGui.Widgets/Combo.cs:23,25 |
Enum.GetValues(typeof(TEnum)), Enum.GetNames(typeof(TEnum)) |
ImGui.Widgets/EnumCombo.cs:44 |
Enum.GetValues<T>() |
ImGui.Widgets/PropertyGridRows.cs:342-343 |
Enum.GetNames<TEnum>(), Enum.GetValues<TEnum>() |
Combo<TEnum> is the worst of the three, because it uses the non-generic overload:
Array possibleValues = Enum.GetValues(typeof(TEnum));
int currentIndex = Array.IndexOf(possibleValues, selectedValue);
...
selectedValue = (TEnum)possibleValues.GetValue(currentIndex)!;
Array.IndexOf(Array, object) boxes selectedValue and every element it compares against, and GetValue boxes again on selection. So a single Combo<TEnum> costs two array allocations plus a boxing allocation per element scanned, every frame.
Combo<TString> (Combo.cs:50) has the same shape for a different reason — it rebuilds the display array with LINQ and calls ToString on every element each frame:
string[] possibleValuesNames = [.. possibleValues.Select(e => e.ToString(CultureInfo.InvariantCulture))];
Suggested fix
Cache per closed generic type in a static holder, which for enums is free after first use because the values never change:
private static class EnumCache<T> where T : struct, Enum
{
public static readonly T[] Values = Enum.GetValues<T>();
public static readonly string[] Names = Enum.GetNames<T>();
}
Then Combo<TEnum> can drop to the generic overload and index without boxing. PropertyGrid.Enum and EnumCombo read the same cache.
The Combo<TString> overload needs a different treatment since its collection is a caller argument — either document that the caller should hoist the name array, or key a small cache on the collection instance.
Notes
- Behaviour should be unchanged. Worth keeping the existing
index >= 0 guard in PropertyGrid.Enum, which is what makes a value outside the declared members (a cast integer, a [Flags] combination) render as no selection rather than throw.
- No public API change.
What
Three widget entry points call
Enum.GetNames/Enum.GetValuesinside the draw path, so every enum combo allocates two fresh arrays per frame, per row, for the life of the application. In an immediate-mode library this is the hot path by definition — a settings panel with a handful of enum rows at 60 fps allocates thousands of arrays a second, all immediately garbage.ImGui.Widgets/Combo.cs:23,25Enum.GetValues(typeof(TEnum)),Enum.GetNames(typeof(TEnum))ImGui.Widgets/EnumCombo.cs:44Enum.GetValues<T>()ImGui.Widgets/PropertyGridRows.cs:342-343Enum.GetNames<TEnum>(),Enum.GetValues<TEnum>()Combo<TEnum>is the worst of the three, because it uses the non-generic overload:Array.IndexOf(Array, object)boxesselectedValueand every element it compares against, andGetValueboxes again on selection. So a singleCombo<TEnum>costs two array allocations plus a boxing allocation per element scanned, every frame.Combo<TString>(Combo.cs:50) has the same shape for a different reason — it rebuilds the display array with LINQ and callsToStringon every element each frame:Suggested fix
Cache per closed generic type in a static holder, which for enums is free after first use because the values never change:
Then
Combo<TEnum>can drop to the generic overload and index without boxing.PropertyGrid.EnumandEnumComboread the same cache.The
Combo<TString>overload needs a different treatment since its collection is a caller argument — either document that the caller should hoist the name array, or key a small cache on the collection instance.Notes
index >= 0guard inPropertyGrid.Enum, which is what makes a value outside the declared members (a cast integer, a[Flags]combination) render as no selection rather than throw.