не ответ: уже есть ответ, он постулирует, что без «коротких» кодов операций размер сборки будет раздуваться. Это правда? Да ... но у нас (у меня, потому что мне нечего было делать этим вечером), чтобы продемонстрировать это :-)
Сколько байтов «получено» с помощью «коротких» (по правде говоря) byte
) значения для OpCodes (например, Bge_S
против Bge
)? И от использования OpCodes, которые включают «фиксированный» индекс/номер? (например, Ldarg_0
против Ldarg
или Ldc_I4_0
и Ldc_I4_M1
против Ldc_I4
)?
Мы могли бы написать программу, чтобы проверить это! :-) Результат для mscorlib 4.5.2:
4.5.2 или более поздняя версия
сборки: C: \ Windows \ Microsoft.NET \ Framework64 \ v4.0.30319 \ mscorlib.dll
Размер: 5217440
Из них ресурсы: 948657
Пропущенные: 9496 методы
Успешно разобран: 63720 методы
Получили 420786 байт из коротких аргументов
Получили 1062014 байт с «фиксированной» номер
Так с «оптимизированными опкодами» mscorlib является 5.2Mb ... Без него было бы 6.6MB только для значения размеров. 25% больше! (и это игнорирование ресурсов в 5,2 МБ, 0,9 МБ ресурсов! Таким образом, процентное усиление в размере кода операции еще больше)
(Я использую mscorlib
, потому что, хотя это не ваша типичная сборка, это, безусловно, достаточно полно кода каждого типа)
программы: (я использую Mono.Reflection):
public static void Main(string[] args)
{
Console.WriteLine(CheckFor45DotVersion(GetReleaseKey()));
string assemblyName = "mscorlib";
Assembly assembly = Assembly.Load(assemblyName);
Console.WriteLine("Assembly: {0}", assembly.Location);
long fullSize = new FileInfo(assembly.Location).Length;
Console.WriteLine("Size: {0}", fullSize);
var allOpCodes = typeof(OpCodes).GetFields(BindingFlags.Static | BindingFlags.Public)
.Where(x => x.FieldType == typeof(OpCode))
.Select(x => (OpCode)x.GetValue(null));
Dictionary<OpCode, int> opcodes = allOpCodes.ToDictionary(x => x, x => 0);
long resourcesLength = 0;
int skippedMethods = 0;
int parsedMethods = 0;
ParseAssembly(assembly, resource =>
{
ManifestResourceInfo info = assembly.GetManifestResourceInfo(resource);
if (info.ResourceLocation.HasFlag(ResourceLocation.Embedded))
{
using (Stream stream = assembly.GetManifestResourceStream(resource))
{
resourcesLength += stream.Length;
}
}
}, method =>
{
if (method.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
{
skippedMethods++;
return;
}
if (method.Attributes.HasFlag(MethodAttributes.PinvokeImpl))
{
skippedMethods++;
return;
}
if (method.IsAbstract)
{
skippedMethods++;
return;
}
parsedMethods++;
IList<Instruction> instructions = method.GetInstructions();
foreach (Instruction instruction in instructions)
{
int num;
opcodes.TryGetValue(instruction.OpCode, out num);
opcodes[instruction.OpCode] = num + 1;
}
});
Console.WriteLine("Of which resources: {0}", resourcesLength);
Console.WriteLine();
Console.WriteLine("Skipped: {0} methods", skippedMethods);
Console.WriteLine("Parsed: {0} methods", parsedMethods);
int gained = 0;
int gainedFixedNumber = 0;
// m1: Ldc_I4_M1
var shortOpcodes = opcodes.Where(x =>
x.Key.Name.EndsWith(".s") ||
x.Key.Name.EndsWith(".m1") ||
// .0 - .9
x.Key.Name[x.Key.Name.Length - 2] == '.' && char.IsNumber(x.Key.Name[x.Key.Name.Length - 1]));
foreach (var @short in shortOpcodes)
{
OpCode opCode = @short.Key;
string name = opCode.Name.Remove(opCode.Name.LastIndexOf('.'));
OpCode equivalentLong = opcodes.Keys.First(x => x.Name == name);
int lengthShort = GetLength(opCode.OperandType);
int lengthLong = GetLength(equivalentLong.OperandType);
int gained2 = @short.Value * (lengthLong - lengthShort);
if (opCode.Name.EndsWith(".s"))
{
gained += gained2;
}
else
{
gainedFixedNumber += gained2;
}
}
Console.WriteLine();
Console.WriteLine("Gained {0} bytes from short arguments", gained);
Console.WriteLine("Gained {0} bytes from \"fixed\" number", gainedFixedNumber);
}
private static int GetLength(OperandType operandType)
{
switch (operandType)
{
case OperandType.InlineNone:
return 0;
case OperandType.ShortInlineVar:
case OperandType.ShortInlineI:
case OperandType.ShortInlineBrTarget:
return 1;
case OperandType.InlineVar:
return 2;
case OperandType.InlineI:
case OperandType.InlineBrTarget:
return 4;
}
throw new NotSupportedException();
}
private static void ParseAssembly(Assembly assembly, Action<string> resourceAction, Action<MethodInfo> action)
{
string[] names = assembly.GetManifestResourceNames();
foreach (string name in names)
{
resourceAction(name);
}
Module[] modules = assembly.GetModules();
foreach (Module module in modules)
{
ParseModule(module, action);
}
}
private static void ParseModule(Module module, Action<MethodInfo> action)
{
MethodInfo[] methods = module.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
foreach (MethodInfo method in methods)
{
action(method);
}
Type[] types = module.GetTypes();
foreach (Type type in types)
{
ParseType(type, action);
}
}
private static void ParseType(Type type, Action<MethodInfo> action)
{
if (type.IsInterface)
{
return;
}
// delegate (in .NET all delegates are MulticastDelegate
if (type != typeof(MulticastDelegate) && typeof(MulticastDelegate).IsAssignableFrom(type))
{
return;
}
MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
foreach (MethodInfo method in methods)
{
action(method);
}
Type[] nestedTypes = type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic);
foreach (Type nestedType in nestedTypes)
{
ParseType(nestedType, action);
}
}
// Adapted from https://msdn.microsoft.com/en-us/library/hh925568.aspx
private static int GetReleaseKey()
{
using (RegistryKey ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\\"))
{
// .NET 4.0
if (ndpKey == null)
{
return 0;
}
int releaseKey = (int)ndpKey.GetValue("Release");
return releaseKey;
}
}
// Checking the version using >= will enable forward compatibility,
// however you should always compile your code on newer versions of
// the framework to ensure your app works the same.
private static string CheckFor45DotVersion(int releaseKey)
{
if (releaseKey >= 393273)
{
return "4.6 RC or later";
}
if ((releaseKey >= 379893))
{
return "4.5.2 or later";
}
if ((releaseKey >= 378675))
{
return "4.5.1 or later";
}
if ((releaseKey >= 378389))
{
return "4.5 or later";
}
// This line should never execute. A non-null release key should mean
// that 4.5 or later is installed.
return "No 4.5 or later version detected";
}
Вдохновленного код Ксанатоса, тот же самый код, используя исходные байты IL может дать нам еще лучше оценить, сколько мы можем получить. Я разделил прибыль/убытки на категории, чтобы получить хорошую картину.
class Program
{
internal class Calculator
{
static Calculator()
{
Register(OpCodes.Beq_S, 1, 3); // category 1: short-hand notations
Register(OpCodes.Bge_S, 1, 3);
Register(OpCodes.Bge_Un_S, 1, 3);
Register(OpCodes.Bgt_S, 1, 3);
Register(OpCodes.Bgt_Un_S, 1, 3);
Register(OpCodes.Ble_S, 1, 3);
Register(OpCodes.Ble_Un_S, 1, 3);
Register(OpCodes.Blt_S, 1, 3);
Register(OpCodes.Blt_Un_S, 1, 3);
Register(OpCodes.Bne_Un_S, 1, 3);
Register(OpCodes.Br_S, 1, 3);
Register(OpCodes.Brfalse_S, 1, 3);
Register(OpCodes.Brtrue_S, 1, 3);
Register(OpCodes.Conv_I, 2, 4); // category 2: types can be generalized
Register(OpCodes.Conv_I1, 2, 4);
Register(OpCodes.Conv_I2, 2, 4);
Register(OpCodes.Conv_I4, 2, 4);
Register(OpCodes.Conv_I8, 2, 4);
Register(OpCodes.Conv_Ovf_I, 2, 4);
Register(OpCodes.Conv_Ovf_I_Un, 2, 4);
Register(OpCodes.Conv_Ovf_I1, 2, 4);
Register(OpCodes.Conv_Ovf_I1_Un, 2, 4);
Register(OpCodes.Conv_Ovf_I2, 2, 4);
Register(OpCodes.Conv_Ovf_I2_Un, 2, 4);
Register(OpCodes.Conv_Ovf_I4, 2, 4);
Register(OpCodes.Conv_Ovf_I4_Un, 2, 4);
Register(OpCodes.Conv_Ovf_I8, 2, 4);
Register(OpCodes.Conv_Ovf_I8_Un, 2, 4);
Register(OpCodes.Conv_Ovf_U, 2, 4);
Register(OpCodes.Conv_Ovf_U_Un, 2, 4);
Register(OpCodes.Conv_Ovf_U1, 2, 4);
Register(OpCodes.Conv_Ovf_U1_Un, 2, 4);
Register(OpCodes.Conv_Ovf_U2, 2, 4);
Register(OpCodes.Conv_Ovf_U2_Un, 2, 4);
Register(OpCodes.Conv_Ovf_U4, 2, 4);
Register(OpCodes.Conv_Ovf_U4_Un, 2, 4);
Register(OpCodes.Conv_Ovf_U8, 2, 4);
Register(OpCodes.Conv_Ovf_U8_Un, 2, 4);
Register(OpCodes.Conv_R_Un, 2, 4);
Register(OpCodes.Conv_R4, 2, 4);
Register(OpCodes.Conv_R8, 2, 4);
Register(OpCodes.Conv_U, 2, 4);
Register(OpCodes.Conv_U1, 2, 4);
Register(OpCodes.Conv_U2, 2, 4);
Register(OpCodes.Conv_U4, 2, 4);
Register(OpCodes.Conv_U8, 2, 4);
Register(OpCodes.Ldarg_0, 3, 2);
Register(OpCodes.Ldarg_1, 3, 2);
Register(OpCodes.Ldarg_2, 3, 2);
Register(OpCodes.Ldarg_3, 3, 2);
Register(OpCodes.Ldarg_S, 1, 2);
Register(OpCodes.Ldarga, 3, 2);
Register(OpCodes.Ldarga_S, 1, 2);
Register(OpCodes.Ldc_I4_0, 3, 2); // category 3: small value loads
Register(OpCodes.Ldc_I4_1, 3, 2);
Register(OpCodes.Ldc_I4_2, 3, 2);
Register(OpCodes.Ldc_I4_3, 3, 2);
Register(OpCodes.Ldc_I4_4, 3, 2);
Register(OpCodes.Ldc_I4_5, 3, 2);
Register(OpCodes.Ldc_I4_6, 3, 2);
Register(OpCodes.Ldc_I4_7, 3, 2);
Register(OpCodes.Ldc_I4_8, 3, 2);
Register(OpCodes.Ldc_I4_M1, 3, 2);
Register(OpCodes.Ldc_I4_S, 1, 3);
Register(OpCodes.Ldelem_I, 2, 4);
Register(OpCodes.Ldelem_I1, 2, 4);
Register(OpCodes.Ldelem_I2, 2, 4);
Register(OpCodes.Ldelem_I4, 2, 4);
Register(OpCodes.Ldelem_I8, 2, 4);
Register(OpCodes.Ldelem_R4, 2, 4);
Register(OpCodes.Ldelem_R8, 2, 4);
Register(OpCodes.Ldelem_Ref, 2, 4);
Register(OpCodes.Ldelem_U1, 2, 4);
Register(OpCodes.Ldelem_U2, 2, 4);
Register(OpCodes.Ldelem_U4, 2, 4);
Register(OpCodes.Ldind_I, 2, 4);
Register(OpCodes.Ldind_I1, 2, 4);
Register(OpCodes.Ldind_I2, 2, 4);
Register(OpCodes.Ldind_I4, 2, 4);
Register(OpCodes.Ldind_I8, 2, 4);
Register(OpCodes.Ldind_R4, 2, 4);
Register(OpCodes.Ldind_R8, 2, 4);
Register(OpCodes.Ldind_Ref, 2, 4);
Register(OpCodes.Ldind_U1, 2, 4);
Register(OpCodes.Ldind_U2, 2, 4);
Register(OpCodes.Ldind_U4, 2, 4);
Register(OpCodes.Ldloc_0, 3, 1);
Register(OpCodes.Ldloc_1, 3, 1);
Register(OpCodes.Ldloc_2, 3, 1);
Register(OpCodes.Ldloc_3, 3, 1);
Register(OpCodes.Ldloc_S, 1, 1);
Register(OpCodes.Ldloca_S, 1, 1);
Register(OpCodes.Leave_S, 1, 3);
Register(OpCodes.Starg_S, 1, 1);
Register(OpCodes.Stelem_I, 2, 4);
Register(OpCodes.Stelem_I1, 2, 4);
Register(OpCodes.Stelem_I2, 2, 4);
Register(OpCodes.Stelem_I4, 2, 4);
Register(OpCodes.Stelem_I8, 2, 4);
Register(OpCodes.Stelem_R4, 2, 4);
Register(OpCodes.Stelem_R8, 2, 4);
Register(OpCodes.Stelem_Ref, 2, 4);
Register(OpCodes.Stind_I, 2, 4);
Register(OpCodes.Stind_I1, 2, 4);
Register(OpCodes.Stind_I2, 2, 4);
Register(OpCodes.Stind_I4, 2, 4);
Register(OpCodes.Stind_I8, 2, 4);
Register(OpCodes.Stind_R4, 2, 4);
Register(OpCodes.Stind_R8, 2, 4);
Register(OpCodes.Stind_Ref, 2, 4);
Register(OpCodes.Stloc_0, 3, 1);
Register(OpCodes.Stloc_1, 3, 1);
Register(OpCodes.Stloc_2, 3, 1);
Register(OpCodes.Stloc_3, 3, 1);
Register(OpCodes.Stloc_S, 1, 1);
}
private Calculator() { }
private static void Register(OpCode opCode, int category, int delta)
{
dict[opCode] = new KeyValuePair<int, int>(category, delta);
}
private static Dictionary<OpCode, KeyValuePair<int, int>> dict = new Dictionary<OpCode, KeyValuePair<int, int>>();
public static void Update(int[] data, OpCode opcode)
{
KeyValuePair<int, int> kv;
if (dict.TryGetValue(opcode, out kv))
{
data[kv.Key] += kv.Value;
}
}
}
internal class Decompiler
{
public Decompiler() { }
static Decompiler()
{
singleByteOpcodes = new OpCode[0x100];
multiByteOpcodes = new OpCode[0x100];
FieldInfo[] infoArray1 = typeof(OpCodes).GetFields();
for (int num1 = 0; num1 < infoArray1.Length; num1++)
{
FieldInfo info1 = infoArray1[num1];
if (info1.FieldType == typeof(OpCode))
{
OpCode code1 = (OpCode)info1.GetValue(null);
ushort num2 = (ushort)code1.Value;
if (num2 < 0x100)
{
singleByteOpcodes[(int)num2] = code1;
}
else
{
if ((num2 & 0xff00) != 0xfe00)
{
throw new Exception("Invalid opcode: " + num2.ToString());
}
multiByteOpcodes[num2 & 0xff] = code1;
}
}
}
}
private static OpCode[] singleByteOpcodes;
private static OpCode[] multiByteOpcodes;
public int[] Delta = new int[5];
public void Decompile(MethodBase mi, byte[] ildata)
{
Module module = mi.Module;
int position = 0;
while (position < ildata.Length)
{
OpCode code = OpCodes.Nop;
ushort b = ildata[position++];
if (b != 0xfe)
{
code = singleByteOpcodes[b];
}
else
{
b = ildata[position++];
code = multiByteOpcodes[b];
b |= (ushort)(0xfe00);
Delta[4]++;
}
switch (code.OperandType)
{
case OperandType.InlineBrTarget:
position += 4;
break;
case OperandType.InlineField:
position += 4;
break;
case OperandType.InlineI:
position += 4;
break;
case OperandType.InlineI8:
position += 8;
break;
case OperandType.InlineMethod:
position += 4;
break;
case OperandType.InlineNone:
break;
case OperandType.InlineR:
position += 8;
break;
case OperandType.InlineSig:
position += 4;
break;
case OperandType.InlineString:
position += 4;
break;
case OperandType.InlineSwitch:
int count = BitConverter.ToInt32(ildata, position);
position += count * 4 + 4;
break;
case OperandType.InlineTok:
case OperandType.InlineType:
position += 4;
break;
case OperandType.InlineVar:
position += 2;
break;
case OperandType.ShortInlineBrTarget:
position += 1;
break;
case OperandType.ShortInlineI:
position += 1;
break;
case OperandType.ShortInlineR:
position += 4;
break;
case OperandType.ShortInlineVar:
position += 1;
break;
default:
throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType);
}
Calculator.Update(Delta, code);
}
}
}
static void Main(string[] args)
{
string assemblyName = "mscorlib";
Assembly assembly = Assembly.Load(assemblyName);
int skippedMethods = 0;
int parsedMethods = 0;
Decompiler decompiler = new Decompiler();
long totalbytes = 0;
Console.WriteLine("Assembly: {0}", assembly.Location);
foreach (var type in assembly.GetTypes())
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (method.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall) ||
method.Attributes.HasFlag(MethodAttributes.PinvokeImpl) ||
method.IsAbstract)
{
skippedMethods++;
}
else
{
var body = method.GetMethodBody();
byte[] bytes;
if (body != null && (bytes = body.GetILAsByteArray()) != null)
{
decompiler.Decompile(method, bytes);
++parsedMethods;
totalbytes += bytes.Length;
}
else
{
skippedMethods++;
}
}
}
}
var delta = decompiler.Delta;
Console.WriteLine("{0} methods parsed, {1} methods skipped", parsedMethods, skippedMethods);
Console.WriteLine("- {0} bytes total", totalbytes);
Console.WriteLine("- {0} bytes gained from generalizing short-hand notations", delta[1]);
Console.WriteLine("- {0} bytes gained from generalizing type notations", delta[2]);
Console.WriteLine("- {0} bytes gained from generalizing loads notations", delta[3]);
Console.WriteLine("- {0} bytes lost from multi-byte opcodes", delta[4]);
Console.ReadLine();
}
}
Результаты:
Assembly: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
56117 methods parsed, 10605 methods skipped
- 2489275 bytes total
- 361193 bytes gained from generalizing short-hand notations
- 126724 bytes gained from generalizing type notations
- 618858 bytes gained from generalizing loads notations
- 9447 bytes lost from multi-byte opcodes
Что это говорит? Во-первых, общее число основано на фактических байтах IL методов. Короткие нотные выигрыши - это количество байтов, полученных от использования кодов операций _S
. Обозначения типа нотации - это коды операций, которые можно было бы обобщить, используя токен типа, вместо специализированной операции (например, преобразования, такие как conv_i4
). Прирост нагрузки - это байты, которые будут получены из специализированных функций нагрузки (например, ldc_i4_0
). И, наконец, потому что у них теперь слишком много кодов операций, чтобы поместиться в байте, IL также нуждается в еще нескольких байтах.
Общий выигрыш от всего этого означает, что у нас будет DLL размером 3586603 вместо 2489275 байт, что составляет коэффициент сжатия +/- 30%.
@HansPassant Как вы, вероятно, тоже (и команда JIT определенно сделала), я довольно долго провожу время в инструкции Intel® x64-86 [...]. Одна вещь, которая действительно выделяется, заключается в том, что они начинались с коротких нотных обозначений, и когда набор инструкций рос, так же как и количество очевидных хаков. Мой урок узнал, что есть: не делайте этого, он просто добавит много сложности в долгосрочной перспективе. Имея это в виду, использование стандартного алгоритма сжатия внезапно делает намного больше смысла, чем короткие байт-коды, не так ли? Возможно, я ошибаюсь, мне просто любопытно ... – atlaste
Я нахожусь на тонком льду здесь, прося «почему они так не делают», я понимаю, что ...Мне просто интересно, какие причины имеют исторический опыт. Если причина * является * сжатием, я понимаю, что это правильное решение, которое они явно выбрали. Просто интересно, если это единственный. – atlaste
@atlaste Вы должны помнить, что, возможно, когда они создали язык IL, они думали о встроенных устройствах (где память была премией) и, возможно, о возможности возможного микропроцессора на основе IL-кода (Java имел [их] (http: // ru.wikipedia.org/wiki/Java_processor)) – xanatos