3

我目前正在将 SQL 表行序列化为二进制格式以进行高效存储。我将二进制数据序列化/反序列化为List<object>每行。我正在尝试升级它以使用 POCO,它将动态生成(发出),每列一个字段。

我在网上搜索了几个小时,偶然发现了 EF、T4、ExpandoObject 等 ORM/框架,但所有这些要么使用动态对象(可以动态添加/删除属性),要么只是在编译之前生成一个 POCO。我不能使用模板,因为表的架构在编译时是未知的,并且使用动态对象会过大(而且很慢),因为我知道确切的属性集及其类型。我需要为每个表生成一个 POCO,字段对应于列,并相应地设置数据类型(INT -> int,TEXT -> string)。

生成 POCO 后,我将继续使用发出的 CIL 获取/设置属性,就像PetaPoco 为静态编译的 POCO 所做的那样。我希望所有这些复杂的操作都比使用无类型列表更快,并为我提供强类型且可以由 CLR 加速的高保真 POCO。我是否正确假设这一点?你可以让我开始在运行时生成 POCO 吗?并且使用 POCO 会比使用 a 更快或更节省内存List<object>吗?基本上,这值得麻烦吗?我已经知道如何使用发出的 CIL 加速获取/设置字段。

4

2 回答 2

10

从评论和聊天来看,这其中的一个关键部分似乎仍然是创建动态类型;好的,这是一个完整的示例,它显示了完全可序列化(通过任何常见的序列化程序)类型。您当然可以向类型添加更多内容 - 也许索引器可以通过数字或名称INotifyPropertyChanged等获取属性。

另外 - 关键点:您必须缓存并重新使用生成的Type实例。不要一直再生这些东西……你会出血的记忆。

using Newtonsoft.Json;
using ProtoBuf;
using System;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Xml.Serialization;

public interface IBasicRecord
{
    object this[int field] { get; set; }
}
class Program
{
    static void Main()
    {
        object o = 1;
        int foo = (int)o;
        string[] names = { "Id", "Name", "Size", "When" };
        Type[] types = { typeof(int), typeof(string), typeof(float), typeof(DateTime?) };

        var asm = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("DynamicStuff"),
            AssemblyBuilderAccess.Run);
        var module = asm.DefineDynamicModule("DynamicStuff");
        var tb = module.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Serializable);
        tb.SetCustomAttribute(new CustomAttributeBuilder(
            typeof(DataContractAttribute).GetConstructor(Type.EmptyTypes), new object[0]));
        tb.AddInterfaceImplementation(typeof(IBasicRecord));

        FieldBuilder[] fields = new FieldBuilder[names.Length];
        var dataMemberCtor = typeof(DataMemberAttribute).GetConstructor(Type.EmptyTypes);
        var dataMemberProps = new[] { typeof(DataMemberAttribute).GetProperty("Order") };
        for (int i = 0; i < fields.Length; i++)
        {
            var field = fields[i] = tb.DefineField("_" + names[i],
                types[i], FieldAttributes.Private);

            var prop = tb.DefineProperty(names[i], PropertyAttributes.None,
                types[i], Type.EmptyTypes);
            var getter = tb.DefineMethod("get_" + names[i],
                MethodAttributes.Public | MethodAttributes.HideBySig, types[i], Type.EmptyTypes);
            prop.SetGetMethod(getter);
            var il = getter.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0); // this
            il.Emit(OpCodes.Ldfld, field); // .Foo
            il.Emit(OpCodes.Ret); // return
            var setter = tb.DefineMethod("set_" + names[i],
                MethodAttributes.Public | MethodAttributes.HideBySig, typeof(void), new Type[] { types[i] });
            prop.SetSetMethod(setter);
            il = setter.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0); // this
            il.Emit(OpCodes.Ldarg_1); // value
            il.Emit(OpCodes.Stfld, field); // .Foo =
            il.Emit(OpCodes.Ret);

            prop.SetCustomAttribute(new CustomAttributeBuilder(
                dataMemberCtor, new object[0],
                dataMemberProps, new object[1] { i + 1 }));
        }

        foreach (var prop in typeof(IBasicRecord).GetProperties())
        {
            var accessor = prop.GetGetMethod();
            if (accessor != null)
            {
                var args = accessor.GetParameters();
                var argTypes = Array.ConvertAll(args, a => a.ParameterType);
                var method = tb.DefineMethod(accessor.Name,
                    accessor.Attributes & ~MethodAttributes.Abstract,
                    accessor.CallingConvention, accessor.ReturnType, argTypes);
                tb.DefineMethodOverride(method, accessor);
                var il = method.GetILGenerator();
                if (args.Length == 1 && argTypes[0] == typeof(int))
                {
                    var branches = new Label[fields.Length];
                    for (int i = 0; i < fields.Length; i++)
                    {
                        branches[i] = il.DefineLabel();
                    }
                    il.Emit(OpCodes.Ldarg_1); // key
                    il.Emit(OpCodes.Switch, branches); // switch
                    // default:
                    il.ThrowException(typeof(ArgumentOutOfRangeException));
                    for (int i = 0; i < fields.Length; i++)
                    {
                        il.MarkLabel(branches[i]);
                        il.Emit(OpCodes.Ldarg_0); // this
                        il.Emit(OpCodes.Ldfld, fields[i]); // .Foo
                        if (types[i].IsValueType)
                        {
                            il.Emit(OpCodes.Box, types[i]); // (object)
                        }
                        il.Emit(OpCodes.Ret); // return
                    }
                }
                else
                {
                    il.ThrowException(typeof(NotImplementedException));
                }
            }
            accessor = prop.GetSetMethod();
            if (accessor != null)
            {
                var args = accessor.GetParameters();
                var argTypes = Array.ConvertAll(args, a => a.ParameterType);
                var method = tb.DefineMethod(accessor.Name,
                    accessor.Attributes & ~MethodAttributes.Abstract,
                    accessor.CallingConvention, accessor.ReturnType, argTypes);
                tb.DefineMethodOverride(method, accessor);
                var il = method.GetILGenerator();
                if (args.Length == 2 && argTypes[0] == typeof(int) && argTypes[1] == typeof(object))
                {
                    var branches = new Label[fields.Length];
                    for (int i = 0; i < fields.Length; i++)
                    {
                        branches[i] = il.DefineLabel();
                    }
                    il.Emit(OpCodes.Ldarg_1); // key
                    il.Emit(OpCodes.Switch, branches); // switch
                    // default:
                    il.ThrowException(typeof(ArgumentOutOfRangeException));
                    for (int i = 0; i < fields.Length; i++)
                    {
                        il.MarkLabel(branches[i]);
                        il.Emit(OpCodes.Ldarg_0); // this
                        il.Emit(OpCodes.Ldarg_2); // value
                        il.Emit(types[i].IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, types[i]); // (SomeType)
                        il.Emit(OpCodes.Stfld, fields[i]); // .Foo =
                        il.Emit(OpCodes.Ret); // return
                    }
                }
                else
                {
                    il.ThrowException(typeof(NotImplementedException));
                }
            }
        }

        var type = tb.CreateType();
        var obj = Activator.CreateInstance(type);
        // we'll use the index (via a known interface) to set the values
        IBasicRecord rec = (IBasicRecord)obj;
        rec[0] = 123;
        rec[1] = "abc";
        rec[2] = 12F;
        rec[3] = DateTime.Now;
        for (int i = 0; i < 4; i++)
        {
            Console.WriteLine("{0} = {1}", i, rec[i]);
        }
        using (var ms = new MemoryStream())
        {
            var ser = new XmlSerializer(type);
            ser.Serialize(ms, obj);
            Console.WriteLine("XmlSerializer: {0} bytes", ms.Length);
        }
        using (var ms = new MemoryStream())
        {
            using (var writer = new StreamWriter(ms, Encoding.UTF8, 1024, true))
            {
                var ser = new JsonSerializer();
                ser.Serialize(writer, obj);
            }
            Console.WriteLine("Json.NET: {0} bytes", ms.Length);
        }
        using (var ms = new MemoryStream())
        {
            var ser = new DataContractSerializer(type);
            ser.WriteObject(ms, obj);
            Console.WriteLine("DataContractSerializer: {0} bytes", ms.Length);
        }
        using (var ms = new MemoryStream())
        {
            Serializer.NonGeneric.Serialize(ms, obj);
            Console.WriteLine("protobuf-net: {0} bytes", ms.Length);
        }
        using (var ms = new MemoryStream())
        {
            // note: NEVER do this unless you have a custom Binder; your
            // assembly WILL NOT deserialize in the next AppDomain (i.e.
            // the next time you load your app, you won't be able to load)
            // - shown only for illustration
            var bf = new BinaryFormatter();
            bf.Serialize(ms, obj);
            Console.WriteLine("BinaryFormatter: {0} bytes", ms.Length);
        }
    }
}

输出:

XmlSerializer: 246 bytes
Json.NET: 81 bytes
DataContractSerializer: 207 bytes
protobuf-net: 25 bytes
BinaryFormatter: 182 bytes
于 2013-05-10T08:59:53.807 回答
3

这实际上是一个相当复杂的问题。不幸的是,要完全回答它,您基本上必须编写它并对其进行测试 - 我强烈建议在您得到答案之前不要查看任何即时 POCO 生成!基本上,您现在应该忽略该步骤。

性能中的另一个基本问题是:它需要多快?我要做的绝对第一件事是绝对最简单的事情,并对其进行测量。最简单的工作是:将其加载到 aDataTable并序列化该数据表(使用RemotingFormat = RemotingFormat.Binary;)。在 10 行代码中,您将在沙中找到一条线:

var dt = new DataTable();
dt.Load(yourDataReader);
//... any access tests
dt.RemotingFormat = SerializationFormat.Binary;
using (var file = File.Create(path))
{
    var bf = new BinaryFormatter();
    bf.Serialize(file, dt);
}
// ... also check deserialize, if that is perf-critical

通常我不会推荐DataTableBinaryFormatter,但是......在这种情况下似乎并不牵强。

就个人而言,我怀疑您会发现DataTable二进制远程处理模式实际上并不可怕。

下一步是在不付出任何巨大努力的情况下看看还有什么其他工作。例如:

所以我很想创建一个说明性的类(纯粹是为了看看它是否更好):

[DataContract]
public class Foo {
    [DataMember(Order=1)] public int Id {get;set;}
    [DataMember(Order=2)] public string Name {get;set;}
    // ... more props
    // IMPORTANT: make this representative - basically, the same data
    // that you had in the data-table

    // note also include any supporting info - any indexers and interface
    // support that your core code needs
}
[DataContract]
public class FooWrapper { // just to help in the test
     [DataMember(Order=1)] public List<Foo> Items {get;set;}
}

并做同样的测试(你的主代码将只使用索引器访问,但现在让 dapper 使用.Query<Foo>(...)API):

var data = conn.Query<Foo>(...).ToList(); // dapper
//... any access tests, just using the indexer API
using (var file = File.Create(path))
{
    var wrapper = new FooWrapper { Items = data };
    Serializer.Serialize(file, wrapper); // protobuf-net
}
// note that you deserialize via Serializer.Deserialize<FooWrapper>(file)

这样做的重点是,这将为您提供一些关于可以实现的合理预期的界限。随意使用您自己的物化器/序列化器来代替 dapper/protobuf-net,但我谦虚地认为,这两个已经针对类似这样的场景进行了大量优化。

当您有一个下限和上限时,您就有合理的数据来回答“是否值得”这个问题。在运行时生成对象并不,但它比大多数人需要做的工作要多。您还需要非常小心地尽可能重用生成的类型。请注意,如果你这条路,protobuf-net 有一个完全非通用的 API,通过Serializer.NonGenericRuntimeTypeModel.Default(所有三个选项最终都在同一个核心)。Dapper没有,但我很乐意添加一个(接受一个Type实例)。在此期间,您还可以将MakeGenericMethod/Invoke用于该步骤。

我意识到我没有直接回答“是否值得”,但这是故意的:如果不直接应用于您的场景,就无法回答。希望我已经提供了一些提示,告诉您如何针对您的场景回答问题。我很想听听你的发现。

只有当您知道这是值得的(并且根据上述情况,我预计这需要大约一个小时的努力),我才会去生成类型的麻烦。如果你这样做了,我推荐使用Sigil——它会让你的 IL 生成变得不那么令人沮丧。

于 2013-05-10T07:21:28.030 回答