Доступ к Range.Start в цикле повышает производительность компаратора

У меня очень странная проблема. Итак, предыстория заключается в том, что у нас есть сопоставление между 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 элементами управления содержимым, и сканирование замедлилось до абсолютного обхода (3+ минуты для создания экземпляра SortedList).

Так что это достаточно странно, но еще большая странность была впереди. Я добавил логирование, чтобы понять, что происходит, и обнаружил, что логирование начала каждого 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<TKey, TValue>(keys, values, comparer); для ключей и значений словаря. Я не могу понять, почему предварительный доступ к объектам Range в цикле ускорит его. Может быть, что-то делать с порядком, в котором они доступны? Цикл foreach будет обращаться к ним в том порядке, в котором они появляются в документе, в то время как Array.Sort будет прыгать повсюду.

Изменить: когда я говорю SortedList, я имею в виду System.Collections.Generic.SortedList<TKey, TValue>. Вот код конструктора, который я использую:

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;            
}

person Zout    schedule 04.04.2016    source источник
comment
Не могли бы вы предоставить код конструктора SortedList? Если именно здесь происходит замедление, это то, что нам нужно увидеть... Известная проблема в Word, между прочим, заключается в том, что часто быстрее использовать цикл, используя for + index вместо foreach. Я думаю, это как-то связано с тем, как Word должен отслеживать, где находятся объекты, а не просто подбирать их с помощью индекса.   -  person Cindy Meister    schedule 05.04.2016
comment
Конечно, я добавил код к своему вопросу. Строка, которая занимает так много времени, это та, о которой я упоминал ранее, вызов Array.Sort. Насколько я понимаю, Array.Sort будет использовать компаратор для сортировки CC на основе начала их диапазона. Я просто не понимаю, почему доступ к Range.Start внезапно становится таким медленным, когда в документе достаточное количество CC, и все же его можно ускорить, предварительно перебирая их. Очень запутанно для моего бедного мозга!   -  person Zout    schedule 05.04.2016


Ответы (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. секунды...

person Maarten van Stam    schedule 05.04.2016
comment
Я также беспокоился об этом (что порядок будет нарушен при изменении содержимого документа), но на самом деле это не проблема, поскольку добавление или удаление текста в точке сместит начало диапазона всех следующих элементов управления содержимым вперед или назад, соответственно. - person Zout; 05.04.2016
comment
Верно, но не будет, если кто-то -переместит- текст. Кроме того, я считаю, что вам не хватает элементов управления содержимым в верхних и нижних колонтитулах документа и/или элементов управления содержимым в фигурах. Прочитайте об этом здесь: stackoverflow.com/questions/4605179/ Будьте очень осторожны, используя объекты управления содержимым, все может стать сложнее, если не протестировать подробно ;-) - person Maarten van Stam; 05.04.2016
comment
Решение для перемещения заключалось в использовании событий ContentControlBeforeDelete ContentControlAfterAdd (которые вызываются при перемещении элемента управления содержимым), чтобы гарантировать, что SortedList остается упорядоченным. Я не знал, что вы можете добавлять CC к фигурам - интересно! Однако я не думаю, что это проблема — содержание, заключенное в CC, несколько стандартизировано, и я считаю, что оно всегда будет в обычной части документа. Что касается ленивой загрузки, у вас есть какие-нибудь идеи, есть ли способ заставить это произойти? Обходной путь ведения журнала кажется мне ужасно хакерским. Спасибо за ваше время, кстати. - person Zout; 05.04.2016
comment
Я провел несколько тестов, может быть, вы сможете проверить, прав ли я. Смотрите редактирование в моем ответе. Я подумал, что было бы нормально использовать Range.Start в качестве значения ключа, думая, что каждый элемент управления содержимым имеет свою собственную начальную позицию, не так ли? Это значительно ускорило мой тест... - person Maarten van Stam; 05.04.2016
comment
Хм... Я думаю, это вызовет проблему, которую вы заметили вначале, когда обновления документа приводят к аннулированию заказа. Это работает прямо сейчас, потому что начальные позиции объектов Range обновляются с изменениями документа, но если бы это было простое целое число, оно осталось бы со старым значением. Запутанная часть этой ситуации заключается в том, что эта операция занимает всего несколько секунд (или меньше) до определенного порога, после чего она занимает несколько минут ... если что-то не происходит с отложенной загрузкой или кэшированием или какой-либо другой магией Office. В идеале я хотел бы, чтобы это волшебство происходило каждый раз! - person Zout; 05.04.2016
comment
Да, этот альтернативный, но более быстрый вариант — это снимок позиций Range.Start. Вы указали, что -порядок- элементов управления не изменится и "повторно просканирован" после добавления или удаления элемента управления содержимым. Это изменилось? Задержка в сохранении -referenced- элементов управления содержимым заключается в том, что он должен пересчитать позицию Range.Start, если она еще не была рассчитана непосредственно перед запросом заданных значений. Так или иначе, Range.Starts являются динамическими, а указатели позиций - очень изменчивы, отсюда и риск, о котором я предупреждал. Я попытаюсь подумать о других альтернативах, поскольку это, безусловно, интересный вариант использования. :-) - person Maarten van Stam; 06.04.2016
comment
Ах, наверное, я не указал, для чего я использую эти события. Я удаляю CC из SortedList в событии BeforeDelete и добавляю его в список в событии AfterAdd. Это гарантирует, что порядок сохраняется, даже если CC перемещается, поскольку он будет удален, а затем снова добавлен на свое место. - person Zout; 06.04.2016