【问题标题】:Creating a checksum on an object graph在对象图上创建校验和
【发布时间】:2011-07-18 08:21:19
【问题描述】:

这个问题和this one有关,但我觉得应该分开问。

我有一个复杂的对象实例图。现在我想在内存中直接在这个对象图上创建一个校验和,以检测自上次将校验和与对象图一起保存以来是否对其进行了更改。校验和计算应该很快并且不应该消耗太多内存。

据我所知,最好的解决方案可能是在对象图的二进制序列化形式上生成一个加密密钥(如果我错了,请纠正我)。但这会带来一些问题:

  1. 我应该如何序列化对象? 一定要快,不能 消耗太多内存。还有它 必须始终可靠地序列化 以同样的方式。 如果我使用 .NET 默认序列化,如果实际数据相同,我真的可以确定创建的二进制流始终相同吗? 我对此表示怀疑。
  2. 那么,有什么替代方法可以实现不需要很长时间的序列化呢?

更新:

您如何看待这种方法:

  1. 浏览图表并 图中的 foreach 对象创建一个 标准 int 哈希码使用 this 算法(但不包括表示图中节点的引用类型成员)。添加每个 哈希码到整数列表
  2. 将整数列表转换为字节 数组
  3. 在字节数组上创建一个散列 使用 MD5、CRC 或类似方法

提到的 GetHashCode 算法应该可以快速计算出一个哈希码,该哈希码对于仅考虑其原始成员的单个对象来说是非常安全的。基于此,字节数组也应该是对象图和 MD5/CRC 散列的相当安全的表示。

【问题讨论】:

  • 不“消耗太多内存”的校验和不能保证检测是否进行了更改。如果您可以忍受一些非常罕见的假阴性(即校验和相同但对象图实际上不同),那么它可能没问题。
  • 如果您将这些问题作为单独的问题提出,您的运气可能会更好。
  • @Justin:是的,校验和不会,但将大对象图序列化为二进制流会。
  • @Justin 也不保证您的 RAM 和 CPU 工作时不会出现随机错误。对于任何体面的校验和(例如 sha-1),计算机产生随机错误的可能性大于碰撞。
  • 序列化整个图(为哈希函数提供更高质量的输入)并使其快速运行是相互矛盾的要求。您应该指定对您更重要的内容,并大致估计您的图形及其序列化表示的大小。

标签: .net serialization checksum


【解决方案1】:

这是我使用的方法:

1。使用ServiceStack's TypeSerializer 进行序列化

这会将对象序列化为 JSV,我将其模糊地描述为“带有更少引号的 JSON”,因此它更小并且(作者)声称比 JSON 序列化快 5 倍。与 BinaryFormatter 和 Protobuff(否则这将是我的首选)相比,主要优势在于您不必到处注释或描述要序列化的所有类型。我就是那样的懒惰,这适用于任何 poco。

2。计算 MD5 哈希

就性能和碰撞特性而言,这对我来说是一种“足够好”的方法。如果我对它进行了改进,我可能会选择MurmurHash3,它具有与 MD5 相似的碰撞特性,但速度要快得多。它不适合加密目的,但听起来这不是这里的要求。我使用 MD5 的唯一原因是它在 BCL 中烘焙,并且对于我的目的来说足够快。

这是作为扩展方法的全部内容:

using System.Text;
using System.Security.Cryptography;
using ServiceStack.Text;

public static byte[] GenerateHash(this object obj) {
    var s = TypeSerializer.SerializeToString(obj);
    return MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(s));
}

我使用的对象相对较小(通常不超过几百个序列化字符),而且我从未遇到过碰撞问题。 YMMV。

【讨论】:

  • 序列化为字符串?这推断我可以创建一个 JSON 字符串并从中获取 MD5 哈希。对吗?
【解决方案2】:

正如 Ira Baxter 所说,您希望以特定的规范顺序重新排列(排序)图中的对象。然后你可以继续计算哈希并将它们减少(如'map-reduce')到单个哈希。

作为一种性能技巧,有时尝试始终以这种方式保持图表也很好 - 有时保持集合排序比在更新事务之后对所有集合进行排序更容易。

您可以在这里使用技巧来最小化内存和 CPU 使用率。您需要分析对象和图形多久更改一次,以及您想知道对象图形是否更改的频率。

正如我在对您的问题的评论中提到的,MD5 和类似的哈希算法不会使用太多内存——每个实例不到一千字节。您一次只需要保留一个数据块(512 字节)进行哈希处理。

如果幸运的话,您的对象和图表会发生很大变化(即许多对象一个接一个地改变状态),但您只想偶尔了解一下(即仅在整个图表之后-更新交易结束)。在这种情况下,您只想在事务结束后计算哈希值。或者可能仅在需要时(即当您推送更新事件或轮询它以从单独的线程中进行更改时)。在这种情况下,为了节省内存,您希望向 MD5/SHAxxx 哈希计算对象提供数据块流,以保持尽可能少的中间值。这样,您的内存使用量将是恒定的,与图形大小无关(如 O(1))。

现在,如果您更幸运,您的对象不会发生太大变化(如果有的话),但是您想知道它们是否立即发生了变化,例如,通过为每次更改引发一个事件。在这种情况下,您想要修改(即包装或以其他方式扩展)对象以计算散列或只是检查它们的实际更改。在对象的每个属性设置器中推送一个“更改”事件。图形变化也一样。这将使您免于计算哈希值(在某些情况下会大幅提升性能)。

如果您的对象很少更改并且您还需要检查它们的更改很少(包括在流程中某处使用反序列化/序列化的情况),那么第一种方法仍然最有效。

尝试为经常变化的图表中的复杂对象计算哈希值通常会适得其反,以便立即了解内部发生的每一个变化(对其中的每一个变化采取行动)。在这种情况下,您希望通过事件(最适合 .NET)或回调来创建某种更改信号方法。

【讨论】:

  • 按需对 ~~3000 个对象进行排序并不昂贵,尤其是考虑到只有在需要校验和时才会这样做,而我认为这种情况很少见。
  • @Ira Baxter 该图中没有提到约 3000 个项目,所以我不太确定。你怎么知道,bitbonk指的是什么样的图?例如,为什么不能是一个大洲的详细道路地图或一个非常大的社交网络的连接图?还是我错过了 cmets 中的某个细节?
  • 是的,有。请参阅有关 xanatos 答案的 bitbonk 评论。
【解决方案3】:

您如何看待这种方法:

  • 在图表中导航,并为图表中的每个对象使用此算法创建标准 int 哈希码(但不包括表示图表中节点的引用类型成员)。
  • 将每个哈希码添加到整数列表中
  • 将整数列表转换为字节数组
  • 使用 MD5、CRC 或类似方法在字节数组上创建哈希

这种方法的想法非常接近我认为最好的方法,但它可能需要一些改进。

散列

考虑到您更喜欢速度而不是准确性,并且每个项目的int 大小的哈希码为避免冲突留下了足够的空间,哈希码算法的选择似乎是正确的。排除参与图表的引用类型意味着我们丢弃了一些信息;有关更多信息,请参见下文。

改进节点哈希

不考虑连接到我们正在散列的节点的其他节点的想法是正确的,但也许我们可以做得比简单地丢弃所有这些信息更好?我们不想考虑其他节点的哈希码(它们自己也会被哈希),但是我们在这里丢弃了图 edges 提供的信息:节点的哈希码内部数据 X 连接到其他 N 个节点的节点不应与数据 X 连接到其他 M 个节点的节点相同。

如果您有使用部分边缘数据的廉价方法,请使用它。例如,如果图是有向图,那么您可以在为每个节点计算的哈希码中添加从该节点到其他节点的边数。

聚合哈希码

创建哈希码列表将是在一个 long 中将哈希码相加(速度非常快,并且在汇总到 int 中保留一些额外信息)和创建一个哈希码列表取决于图中项目的总顺序。如果您期望图中有很多项目,那么求和可能更合适(我会先尝试一下,看看它是否足够无冲突);如果图表没有很多项目(比如 记得在创建列表时为列表分配足够的内存(或者干脆使用数组);你已经知道它的最终长度,所以这是一个免费的速度提升。

生成固定大小的哈希

如果您已将哈希码汇总为一个原语,则根本不需要此步骤。否则,我认为最好将列表散列为byte[]。由于与创建列表相比,对字节进行哈希处理所需的时间非常短,因此您可能希望使用比 md5 或 crc32 更大的哈希函数来减少冲突而不影响实际性能。

提高最终哈希质量

在获得这个“最终”哈希后,我会将哈希图中的项目数作为固定大小的十六进制编码字符串添加或附加到它,因为:

  • 它可能有助于减少冲突(具体程度取决于图表的性质)
  • 我们已经知道图中项目的数量(我们只是对每个项目进行哈希处理),所以这是一个 O(1) 操作

定义总订单

如果图中项目的处理顺序没有严格定义,那么门就为假阴性打开了:两个应该散列到相同值的图没有,因为即使它们在逻辑上是等价的,实现散列函数选择以不同的顺序处理每个项目的散列。仅当您使用列表时才会出现此问题,因为添加是可传递的,因此“添加到 long 方法”不受它的影响。

为了解决这个问题,您需要以明确定义的顺序处理图中的节点。这可能是一个很容易从节点的数据结构(例如,树上的前序遍历)和/或其他信息(例如,每个节点的类名或节点类型、节点 ID(如果存在)等)生成的顺序。

由于预处理图表以生成总订单需要一些时间,因此您可能需要权衡一下我上面提到的假阴性结果所产生的成本。此外,如果图表足够大,那么这个讨论可能没有实际意义,因为节点哈希码求和方法更适合您的需求。

【讨论】:

  • 投反对票者:如果您发现此答案有缺陷,我愿意接受批评。
【解决方案4】:

我认为您想要的是为对象生成规范顺序,按该顺序对对象进行排序,然后按该排序顺序计算对象的哈希值。

一种方法是定义对象之间的关系,如果对象不包含相同的内容,则始终为“”(在这种情况下,根据关系,对象为“==”)[注意:这并没有说明来自相同内容对象的弧可能允许您将它们区分为“”的事实;如果这对您很重要,请在弧上定义规范顺序] 现在,枚举图中的所有对象,并按此关系排序。按排序顺序处理对象,并组合它们的哈希值。

我希望它运行得非常快,肯定比任何涉及序列化的解决方案都要快得多,因为它不会从值生成巨大的文本(甚至是二进制)字符串。

【讨论】:

  • 对我来说,这似乎与我在更新我的问题时所做的非常接近。现在的问题是我将要使用的单个对象的简单哈希码算法是否足够,以及我将如何组成哈希。我是否只需将所有哈希写入 BinaryWriter,然后在该流上创建一个 MD5(或类似)?
  • 这并不重要,如果你想要做的是创建一个校验和。在这种情况下,您所关心的是大多数时候它会很容易地检测到变化;在这种情况下,我可能会简单地添加 64 位校验和并忽略溢出。
  • ...如果你想要加密质量的签名,你可能需要像你建议的那样(事实上,当前的加密人会抱怨你使用 MD5,因为有人发现了一个难以执行的漏洞它)。我不是回答这个问题的合适人选,但肯定已经探索了将子对象的加密质量哈希组合成一个整体的问题。
  • 请注意,您的更新已接近尾声,但错过了关键的 ordering 想法。如果你不这样做,另一个相同的(同构的)结构在内存中以不同的方式布局(例如,在不同的执行期间)将产生不同的哈希,我认为这不是你想要的。如果您愿意接受“仅”一个哈希,并使用对顺序不敏感的哈希组合方案(“添加哈希”是一个),那么您可以不进行排序就离开,否则您不能。
  • 你将如何处理异构对象?
【解决方案5】:

您可以使用http://code.google.com/p/protobuf-net/ 代替二进制序列化,然后计算它的加密哈希。据说 protobuf 比 Bin Ser 更紧凑(参见例如 http://code.google.com/p/protobuf-net/wiki/Performance )。

考虑到您实际上并不需要序列化,我会补充一点。最好使用反射并“导航”通过计算您的哈希的对象(以相同的方式各种序列化程序“遍历”您的对象)。参见例如Using reflection in C# to get properties of a nested object

经过深思熟虑,听了@Jon 的话,我可以告诉你,我的“次要”想法(使用反射)非常非常非常困难,除非你想花一周时间编写对象解析器。是的,它是可行的......但是在计算哈希之前你会给数据什么表示?说清楚:

two strings
"A"
"B"

显然是“A”、“B”!=“AB”、“”。但是 MD5("A") 结合了 MD5("B") == MD5("AB") 结合了 MD5("")。可能最好的方法是在前面加上长度(所以使用 Pascal/BSTR 表示法)

还有null 值?他们有什么“序列化”价值?另一个问题。显然,如果您将字符串序列化为长度+字符串(以便解决前面的问题),您可以简单地将 null 序列化为"null"(无长度)......对象呢?你会在前面加上一个对象类型 ID 吗?肯定会更好。否则可变长度对象可能会造成与字符串一样的混乱。

使用 BinaryFormatter(甚至可能是 protobuf-net)您不必真正将序列化对象保存在某处,因为它们都支持流式传输......一个例子

public class Hasher : Stream
{
    protected readonly HashAlgorithm HashAlgorithm;

    protected Hasher(HashAlgorithm hash)
    {
        HashAlgorithm = hash;
    }

    public static byte[] GetHash(object obj, HashAlgorithm hash)
    {
        var hasher = new Hasher(hash);

        if (obj != null)
        {
            var bf = new BinaryFormatter();
            bf.Serialize(hasher, obj);
        }
        else
        {
            hasher.Flush();
        }

        return hasher.HashAlgorithm.Hash;
    }

    public override bool CanRead
    {
        get { throw new NotImplementedException(); }
    }

    public override bool CanSeek
    {
        get { throw new NotImplementedException(); }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void Flush()
    {
        HashAlgorithm.TransformFinalBlock(new byte[0], 0, 0);
    }

    public override long Length
    {
        get { throw new NotImplementedException(); }
    }

    public override long Position
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        HashAlgorithm.TransformBlock(buffer, offset, count, buffer, offset);
    }
}

static void Main(string[] args)
{
    var list = new List<int>(100000000);

    for (int i = 0; i < list.Capacity; i++)
    {
        list.Add(0);
    }

    Stopwatch sw = Stopwatch.StartNew();
    var hash = Hasher.GetHash(list, new MD5CryptoServiceProvider());
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);
}

我定义了一个Hasher 类,它接收对象的序列化(一次一个片段)并以“流模式”计算散列。内存使用量为 O(1)。时间显然是 O(n) (其中 n 是序列化对象的“大小”)。

如果您想使用 protobuf(但请注意,对于复杂的对象,它需要使用其属性(或 WCF 属性或...)标记它们)

public static byte[] GetHash<T>(T obj, HashAlgorithm hash)
{
    var hasher = new Hasher(hash);

    if (obj != null)
    {
        ProtoBuf.Serializer.Serialize(hasher, obj);
        hasher.Flush();
    }
    else
    {
        hasher.Flush();
    }

    return hasher.HashAlgorithm.Hash;
}

唯一的“大”区别是 protobuf 不Flush 流,所以我们必须这样做,并且它确实希望输入根对象而不是简单的“对象”。

哦...对于您的问题:

我应该如何序列化对象?它 一定要快,不要消耗太多 记忆。它还必须始终可靠 以同样的方式序列化。如果我使用 .NET 默认序列化可以 确实确保创建的二进制文件 流总是相同的,如果 实际数据是一样的吗?我怀疑。

List<int> l1 = new List<int>();

byte[] bytes1, bytes2;

using (MemoryStream ms = new MemoryStream())
{
    new BinaryFormatter().Serialize(ms, l1);
    bytes1 = ms.ToArray();
}

l1.Add(0);
l1.RemoveAt(0);

using (MemoryStream ms = new MemoryStream())
{
    new BinaryFormatter().Serialize(ms, l1);
    bytes2 = ms.ToArray();
}

Debug.Assert(bytes1.Length == bytes2.Length);

可以这样说:Debug.Assert 将失败。这是因为 List “保存”了一些内部状态(例如版本)。这使得二进制序列化和比较非常困难。您最好使用“可编程”序列化程序(如 proto-buf)。你告诉他要序列化哪些属性/字段,他会序列化它们。

那么,什么是序列化的替代方法,不需要很长时间来实现?

Proto-buf... 或 DataContractSerializer(但它很慢)。可以想象,数据序列化没有灵丹妙药。

【讨论】:

  • 反射不符合“快速”的条件。
  • @Jon 您可以“缓存”使用反射“一次”发现的对象属性以供以后使用。我不认为它慢得多......最后我认为各种序列化程序都是这样做的。
  • @Jon 或者你可以使用表达式。它们似乎更快stefan.rusek.org/Posts/…您使用反射“探索”对象,然后创建一些表达式来保存它们。
  • @xanatos:我想说的是,使用反射的幼稚实现不会令人满意,并且需要大量的工作才能做到这一点。并不是说它不可行。
  • @Jon 是的......如果你想让它变得更好,可能至少需要一周的工作......我环顾四周,我还没有找到任何“预制”对象解析器...而且我什至不确定您应该如何拆分字段... Null 和可变长度对象让我很适合(“A”,“B”!=“AB”,“”)
猜你喜欢
  • 1970-01-01
  • 2021-11-09
  • 2015-09-10
  • 2017-07-01
  • 1970-01-01
  • 2021-10-01
  • 2021-04-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多