Я работаю над внутренней библиотекой, которая должна поддерживать как списки, так и массивы. Это горячий путь, поэтому мы ищем улучшения производительности.
В настоящее время в параметрах используется IList<T>
, поскольку нам требуется доступ к библиотеке на основе индекса и чтобы избежать перезаписи для обоих типов.
public int Foo<T>(IList<T> values);
Довольно бесплатным улучшением производительности (измеренным и протестированным) было бы внутреннее преобразование его в диапазон, что мы могли бы сделать, просто создав несколько перегрузок, которые преобразуют значение и немедленно передают его, но кажется, что должен быть более чистый способ. Я не большой поклонник большого количества заглушек, особенно когда кажется, что они делают очень похожую вещь.
public int Foo<T>(T[] values)
=> Foo(values.AsSpan());
public int Foo<T>(List<T> values)
=> Foo(CollectionsMarshal.AsSpan(values));
private int Foo<T>(Span<T> values)
Есть ли какой-либо интерфейс/маршал, который можно использовать как для списка, так и для массива, который преобразует их в диапазон?
Невозможно использовать один и тот же метод AsSpan()
для списков и массивов, кроме как создать собственное расширение AsSpan()
. Вместо этого вы всегда можете использовать IList<T>
:
public int Foo<T>(IList<T> list)
{
Span<T> span;
if (list is T[] array)
{
span = array.AsSpan();
}
else if (list is List<T> concreteList)
{
span = CollectionsMarshal.AsSpan(concreteList);
}
else
{
throw new NotSupportedException("not supported!");
}
return Foo(span);
}
private int Foo<T>(Span<T> values)
{
// ...
}
Довольно бесплатное улучшение производительности
Я бы не сказал, что это совсем бесплатно. Возможно, вам стоит прочитать мелким шрифтом на CollectionsMarshal.AsSpan<T>(List<T>)
Получает
Span<T>
представление данных в списке. Предметы не должны быть добавлен или удален изList<T>
, пока используется Span.
Это связано с реализацией (которая на самом деле является лучшим, что можно сделать из-за того, как реализован List<T>
):
T[] items = list._items; // this is the backing store of the list which could change if not enough capacity
...
span = new Span<T>(ref MemoryMarshal.GetArrayDataReference(items), size);
что действительно нарушает определение того, что IList<T>
должен уметь делать:
void Insert(int index, T item);
void RemoveAt(int index);
На этом этапе мы можем рассматривать IReadOnlyList<T>
как обходной путь, но это не меняет того факта, что пока мы преобразуем экземпляр List<T>
в Span<T>
внутри метода (путем получения ссылки на резервное хранилище списка), кто-то другой может изменить оригинал List<T>
до такой степени, что возникнет необходимость в новом хранилище резервных копий из-за ограничений емкости. Таким образом, наш метод будет работать со старыми/недействительными/противоречивыми данными.
Итак, я бы просто добавил перегрузку, принимающую Span<T>
для повышения производительности, и не делал никаких изменений в существующем методе. Или, если вы действительно хотите облегчить жизнь людям, использующим библиотеку, добавьте:
/// <summary>
/// Some good warning...
/// </summary>
public int FooDangerous<T>(IList<T> values) {
if (values is List<T> valuesList) {
var span = CollectionsMarshal.AsSpan((List<T>)valuesList);
return Foo(span);
} else if (values is T[] valuesArray) {
var span = valuesArray.AsSpan();
return Foo(span);
}
// logic
}
Это имеет смысл, спасибо. Мне было интересно, почему у List<T>
другой механизм преобразования в диапазон, чем тот, который используется для массива. В этом контексте мы всегда ожидаем, что коллекции будут иметь все свои данные (правда, они, вероятно, должны быть IReadOnlyList<T>
), поэтому мне не приходило в голову, что произойдет, если список будет изменен в другом месте. Спасибо!
@ JBrown521, да, я думаю, что это одна из (если не главная) причина, по которой нет удобного интерфейса с добавлением GetSpan()
к основной коллекции/списку интерфейсов BCL, хотя Span<T>
у нас уже давно есть.
Да, это то, на чем мы остановились, но особой радости это не вызывает. Спасибо!