【问题标题】:Accessing Range.Start in Loop Improves Performance Of Comparer访问 Range.Start in Loop 提高了比较器的性能
【发布时间】:2016-04-04 20:07:26
【问题描述】:

我遇到了一个非常奇怪的问题。因此,背景是我们在 Word ContentControl 和一个自定义对象之间建立了映射,我们使用该对象来存储与该控件内的内容相关的一些信息。我们使用SortedList<ContentControl, OurCustomObject> 来维护这个映射。 SortedList 部分对于能够找到下一个/上一个内容控件以及能够快速访问与内容控件关联的对象很有用。

要进行设置,我们执行以下操作:

var dictOfObjs = Globals.ThisAddIn.Application.ActiveDocument.ContentControls
    .Cast<ContentControl>()
    .ToDictionary(key => key, elem => new OurCustomObject(elem));
var comparer = Comparer<ContentControl>
    .Create((x, y) => x.Range.Start.CompareTo(y.Range.Start));
var list = new SortedList<ContentControl, OurCustomObject>(dictOfObjs, storedcomparer);

这似乎工作得很好,但我最近在一个包含大约 5000 个内容控件的文档上尝试了它,它减慢到绝对爬行(实例化 SortedList 需要 3 多分钟)。

所以这已经够奇怪了,但更多的奇怪还在后面。我添加了一些日志记录以了解发生了什么,并发现在列表中使用它们之前记录每个 ContentControl 的开始会使它加快约 60 倍。 (是的,添加日志记录加快了速度!)。这是更快的代码:

var dictOfObjs = Globals.ThisAddIn.Application.ActiveDocument.ContentControls
    .Cast<ContentControl>()
    .ToDictionary(key => key, elem => new OurCustomObject(elem));

foreach (var pair in dictOfObjs)
{
    _logger.Debug("Start: " + pair.Key.Range.Start);
}

var comparer = Comparer<ContentControl>
    .Create((x, y) => x.Range.Start.CompareTo(y.Range.Start));
var list = new SortedList<ContentControl, OurCustomObject>(dictOfObjs, storedcomparer);

SortedList 的构造函数在字典的键和值上调用Array.Sort&lt;TKey, TValue&gt;(keys, values, comparer);。我不明白为什么事先在循环中访问 Range 对象会加快速度。也许与访问它们的顺序有关? foreach 循环将按照它们在文档中出现的顺序访问它们,而 Array.Sort 将四处跳来跳去..

编辑:当我说 SortedList 时,我的意思是 System.Collections.Generic.SortedList&lt;TKey, TValue&gt;。这是我正在使用的构造函数的代码:

public SortedList(IDictionary<TKey, TValue> dictionary, IComparer<TKey> comparer) 
    : this((dictionary != null ? dictionary.Count : 0), comparer) {
    if (dictionary==null)
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);

    dictionary.Keys.CopyTo(keys, 0);
    dictionary.Values.CopyTo(values, 0);
    Array.Sort<TKey, TValue>(keys, values, comparer);
    _size = dictionary.Count;            
}

【问题讨论】:

  • 能否提供 SortedList 构造函数代码?如果这就是减速发生的地方,那就是我们需要看到的...... Word 中的一个已知问题 fwiw 是使用 for + index 而不是 foreach 循环通常更快。我认为这与 Word 如何“跟踪”对象的位置有关,而不是简单地使用索引来获取它。
  • 当然 - 我将代码添加到我的问题中。需要这么长时间的线路是我之前提到的那条线路,即对Array.Sort 的调用。我的理解是Array.Sort 将使用比较器根据它们的范围开始对 CC 进行排序。我只是不明白为什么一旦文档中有足够数量的 CC 访问Range.Start 会突然变得如此缓慢,但可以通过事先迭代它们来加快速度。让我可怜的大脑感到非常困惑!

标签: c# ms-word ms-office vsto comobject


【解决方案1】:

除了您的性能问题之外,我认为您的解决方案(除非文档实际上是静态文档)会随着时间的推移而失败。 Range.Start 位置往往会根据在文档中添加/删除内容而发生变化。

为了证明这一点,添加以下小宏并从 VBA 运行它:

Sub testccstart()

    Dim cc As ContentControl
    Set cc = ActiveDocument.ContentControls.Add(wdContentControlRichText)

    MsgBox cc.Range.Start

    ActiveDocument.Range(0).InsertBefore "Blablabla"

    MsgBox cc.Range.Start

End Sub

当您在文档开头输入文本时,您会注意到内容控件的 Range.Start 从 1 变为 10。因此,文档中的所有编辑都要求您重新加载基于 Range.Start 的列表。

不过,您的问题的答案是,通过添加日志记录,您触发了一些他们所谓的“延迟加载”(仅在实际访问时加载内容)。 Office 中的各种优化并不总是很清楚(例如,使用数组访问 Excel 范围通常要快得多)。

我测试了一下,想知道这是否适合你:

 var dictOfObjs = document.ContentControls
                        .Cast<ContentControl>()
                        .ToDictionary(key => key.Range.Start, elem => new OurCustomObject(elem));

 var comparer = Comparer<int>.Create((x, y) => x.CompareTo(y));

 var list = new SortedList<int, OurCustomObject>(dictOfObjs, comparer);

而不是使用内容控件作为键(我猜你已经将内容控件存储在 OurCustomObject 中?)假设每个内容控件都有一个唯一的开始位置,使用开始位置作为键,这使我对 1600 个项目的处理从48到5秒...

【讨论】:

  • 我也担心这一点(随着文档内容的变化,排序会被破坏)——但这实际上不是问题,因为在某个点添加或删除文本会改变范围开始以下所有内容控件分别向前或向后。
  • 是的,但如果有人移动文本,则不会。另外,我相信您缺少文档页眉和页脚中的内容控件和/或形状中的内容控件。在此处阅读:stackoverflow.com/questions/4605179/… 使用 Content Control 对象时要非常小心,如果不进行详细测试,事情会变得很棘手;-)
  • 移动的解决方案是使用 ContentControlBeforeDelete ContentControlAfterAdd 事件(在移动内容控件时调用)来确保 SortedList 保持有序。我没有意识到您可以将 CC 添加到形状中 - 很有趣!不过,我认为这不是问题 - 由 CC 包装的内容在某种程度上是标准化的,我相信它将始终位于文档的正常部分。关于延迟加载,您是否知道是否有办法强制它发生?日志记录“解决方法”对我来说似乎非常糟糕。谢谢你的时间,顺便说一句。
  • 我做了一些测试,也许你可以检查我是否正确。请参阅我的答案中的编辑。我认为可以使用 Range.Start 作为 Key 值,认为每个内容控件都有自己的起始位置,你不觉得吗?这大大加快了我的测试速度......
  • 嗯...我认为这会导致您最初注意到的问题,即对文档的更新会导致订单无效。它现在可以工作,因为 Range 对象的起始位置会随着文档更改而更新,但如果它是一个普通整数,它将保留旧值。这种情况令人困惑的部分是,此操作只需要几秒钟(或更少)达到某个阈值,之后需要几分钟......除非发生延迟加载或缓存或其他一些 Office 魔法。理想情况下,我希望每次都能发生这种魔法!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-18
  • 1970-01-01
  • 1970-01-01
  • 2014-04-19
  • 1970-01-01
相关资源
最近更新 更多