Выполняя некоторые причудливые генерации кода, я столкнулся с переполнением стека, которое я не понимаю.
Мой код в основном такой:
static Tuple<string, int>[] DoWork()
{
// [ call some methods ]
Tuple<string, int>[] tmp = new Tuple<string, int>[100];
tmp[0] = new Tuple<string, int>("blah 1", 0);
tmp[1] = new Tuple<string, int>("blah 2", 1);
tmp[2] = new Tuple<string, int>("blah 3", 2);
// ...
tmp[99] = new Tuple<string, int>("blah 99", 99);
return tmp;
}
Если вы используете небольшие цифры, как здесь (100), все работает нормально. Если цифры большие, то случаются странные вещи. В моем случае я попытался испустить примерно 10K строк кода, подобных этому, что вызвало исключение.
Итак... почему я думаю, что это странно:
- tmp является локальным ссылочным типом, поэтому я ожидаю, что в куче будет выделен только указатель.
- Кортежи являются ссылочными типами и выделяются в куче.
- Нет рекурсии или другой странности; afaik требования к хранению в куче должны быть ограничены.
Воспроизведение странности...
Я не могу воспроизвести stackoverflow в минимальном тестовом сценарии, но я заметил, что он запускается на 64-разрядной версии .NET 4.5. Я могу дать некоторые доказательства, демонстрирующие, что происходит.
Также обратите внимание, что в реальном коде используется код Reflection.Emit
, который генерирует это генерирование кода... он не похож на сам код, который имеет все эти строки кода... Испускаемый код IL правильный BTW.
В Visual Studio - поставить точку останова на последней строке. Обратите внимание на использование указателя стека при разборке (ASM, а не IL).
Теперь добавьте новую строку в код - например. tmp[100] = // the usuals
. Поместите здесь точку останова и обратите внимание, что используемое пространство стека растет.
Что касается попытки воспроизвести использование минимального тестового примера с помощью Reflection.Emit
, это код (который НЕ воспроизводит проблему как ни странно), но очень близок к тому, что я сделал, чтобы вызвать переполнение стека... это должно дать немного картины, что я пытаюсь сделать, и, возможно, кто-то другой может создать жизнеспособный тестовый пример, используя это). Здесь:
public static void Foo()
{
Console.WriteLine("Foo!");
}
static void Main(string[] args)
{
// all this just to invoke one opcode with no arguments!
var assemblyName = new AssemblyName("MyAssembly");
var assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.RunAndCollect);
// Create module
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule");
var type = moduleBuilder.DefineType("MyType", TypeAttributes.Public, typeof(object));
var method = type.DefineMethod("Test", System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.Static, System.Reflection.CallingConventions.Standard, typeof(Tuple<string, int>[]), new Type[0]);
ILGenerator gen = method.GetILGenerator();
int count = 0x10000;
gen.Emit(OpCodes.Call, typeof(StackOverflowGenerator).GetMethod("Foo"));
var loc = gen.DeclareLocal(typeof(Tuple<string, int>[]));
gen.Emit(OpCodes.Ldc_I4, count);
gen.Emit(OpCodes.Newarr, typeof(Tuple<string, int>));
gen.Emit(OpCodes.Stloc, loc);
for (int i = 0; i < count; ++i)
{
// Load array
gen.Emit(OpCodes.Ldloc, loc);
gen.Emit(OpCodes.Ldc_I4, i);
// Construct tuple:
gen.Emit(OpCodes.Ldstr, "This is the string");
gen.Emit(OpCodes.Ldc_I4, i);
gen.Emit(OpCodes.Newobj, typeof(Tuple<string, int>).GetConstructor(new[] { typeof(string), typeof(int) }));
// Store in the array
gen.Emit(OpCodes.Stelem_Ref);
}
// Return the result
gen.Emit(OpCodes.Ldloc, loc);
gen.Emit(OpCodes.Ret);
var materialized = type.CreateType();
var tmp = checked((Tuple<string, int>[])materialized.GetMethod("Test").Invoke(null, new object[0]));
int total = 0;
foreach (var item in tmp)
{
total += item.Item1.Length + item.Item2;
}
Console.WriteLine("Total: {0}", total);
Console.ReadLine();
}
Мой вопрос
Как это может произойти, например, SOE? Что здесь происходит? Почему в этом контексте все вещи помещаются в стек?