Сравнение производительности IEnumerable и повышение события для каждого элемента в источнике?

Я хочу прочитать большой двоичный файл, содержащий миллионы записей, и я хочу получить некоторые отчеты для записей. я использую BinaryReader читать (что, я думаю, имеет лучшую производительность в читателях) и преобразовывать прочитанные байты в модель данных. Из-за количества записей передача модели на уровень отчета является еще одной проблемой: я предпочитаю использовать IEnumerable иметь функциональные возможности и функции LINQ при разработке отчетов.

Вот пример класса данных:

Public Class MyData
    Public A1 As UInt64
    Public A2 As UInt64
    Public A3 As Byte
    Public A4 As UInt16
    Public A5 As UInt64
End Class

Я использовал этот саб для создания файла:

Sub CreateSampleFile()
    Using streamWriter As New FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.Write)
        For i As Integer = 1 To 1000
            For j As Integer = 1 To 1000
                For k = 1 To 30
                    Dim item As New MyData With {.A1 = i, .A2 = j, .A3 = k, .A4 = j, .A5 = i * j}
                    Dim bytes() As Byte = BitConverter.GetBytes(item.A1).Concat(BitConverter.GetBytes(item.A2)).Concat({item.A3}).Concat(BitConverter.GetBytes(item.A4)).Concat(BitConverter.GetBytes(item.A5)).ToArray
                    streamWriter.Write(bytes, 0, bytes.Length)
                Next
            Next
        Next
    End Using
End Sub

И вот мой класс читателя:

Imports System.IO

Public Class FileReader

    Public Const BUFFER_LENGTH As Long = 4096 * 256 * 27
    Public Const MY_DATA_LENGTH As Long = 27
    Private _buffer(BUFFER_LENGTH - 1) As Byte
    Private _streamWriter As FileStream
    Public Event OnByteRead(sender As FileReader, bytes() As Byte, index As Long)

    Public Sub StartReadBinary(fileName As String)
        Dim currentBufferReadCount As Long = 0
        Using fileStream As New FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)
            Using streamReader As New BinaryReader(fileStream)
                currentBufferReadCount = streamReader.Read(Me._buffer, 0, Me._buffer.Length)
                While currentBufferReadCount > 0
                    For i As Integer = 0 To currentBufferReadCount - 1 Step MY_DATA_LENGTH
                        RaiseEvent OnByteRead(Me, Me._buffer, i)
                    Next
                    currentBufferReadCount = streamReader.Read(Me._buffer, 0, Me._buffer.Length)
                End While
            End Using
        End Using
    End Sub

    Public Iterator Function GetAll(fileName As String) As IEnumerable(Of MyData)
        Dim currentBufferReadCount As Long = 0
        Using fileStream As New FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)
            Using streamReader As New BinaryReader(fileStream)
                currentBufferReadCount = streamReader.Read(Me._buffer, 0, Me._buffer.Length)
                While currentBufferReadCount > 0
                    For i As Integer = 0 To currentBufferReadCount - 1 Step MY_DATA_LENGTH
                        Yield GetInstance(_buffer, i)
                    Next
                    currentBufferReadCount = streamReader.Read(Me._buffer, 0, Me._buffer.Length)
                End While
            End Using
        End Using
    End Function

    Public Function GetInstance(bytes() As Byte, index As Long) As MyData
        Return New MyData With {.A1 = BitConverter.ToUInt64(bytes, index), .A2 = BitConverter.ToUInt64(bytes, index + 8), .A3 = bytes(index + 16), .A4 = BitConverter.ToUInt16(bytes, index + 17), .A5 = BitConverter.ToUInt64(bytes, index + 19)}
    End Function

End Class

Я думал о IEnumerable производительность, поэтому я попытался использовать оба GetAll метод как IEnumerable и поднятие события для каждой записи, которая читается из файла. Вот тестовый модуль:

Imports System.IO

Module Module1

    Private fileName As String = "MyData.dat"
    Private readerJustTraverse As New FileReader
    Private WithEvents readerWithoutInstance As New FileReader
    Private WithEvents readerWithInstance As New FileReader
    Private readerIEnumerable As New FileReader

    Sub Main()

        Dim s As New Stopwatch

        s.Start()
        readerJustTraverse.StartReadBinary(fileName)
        s.Stop()
        Console.WriteLine("Read bytes: {0}", s.ElapsedMilliseconds)

        s.Restart()
        readerWithoutInstance.StartReadBinary(fileName)
        s.Stop()
        Console.WriteLine("Read bytes, raise event: {0}", s.ElapsedMilliseconds)

        s.Restart()
        readerWithInstance.StartReadBinary(fileName)
        s.Stop()
        Console.WriteLine("Read bytes, raise event, get instance: {0}", s.ElapsedMilliseconds)

        s.Restart()
        For Each item In readerIenumerable.GetAll(fileName)

        Next
        Console.WriteLine("Read bytes, get instance, return yield: {0}", s.ElapsedMilliseconds)
        s.Stop()

        Console.ReadLine()

    End Sub

    Private Sub readerWithInstance_OnByteRead(sender As FileReader, bytes() As Byte, index As Long) Handles readerWithInstance.OnByteRead
        Dim item As MyData = sender.GetInstance(bytes, index)
    End Sub

    Private Sub readerWithoutInstance_OnByteRead(sender As FileReader, bytes() As Byte, index As Long) Handles readerWithoutInstance.OnByteRead
        'do nothing
    End Sub

End Module

Что мне интересно, так это время, затраченное на каждый процесс, вот результат теста (тестировался на ASUS Ultrabook - Zenbook Core i7):

Прочитано байтов: 384 (не касаясь прочитанных байтов!)

Прочитать байты, поднять событие: 583

Прочитать байты, вызвать событие, получить экземпляр: 3923

Прочитать байты, получить экземпляр, вернуть доход: 4917

Это показывает, что чтение файла как байта невероятно быстро, а преобразование байтов в модель происходит медленно. Также повышение события вместо получения IEnumerable результата происходит на 25% быстрее.

Итерации в IEnumerable действительно стоили этой производительности или я что-то пропустил?

1 ответ

Да, использование функций-итераторов влечет за собой снижение производительности.

Я скомпилировал ваш код и получил те же результаты, что и вы. Я посмотрел на сгенерированный код IL. Конечный автомат, созданный из метода GetAll, содержит много вещей, но большинство инструкций - это nop или простые операции.

Результаты с использованием и без использования итераторов отличаются, как вы говорите, на 25%. Это не так уж много. Когда вы используете StartReadBinary, есть просто один большой цикл, который вызывает метод OnByteRead (через событие) три миллиарда раз. Однако, когда вы создаете объекты в цикле foreach, для каждого объекта вы должны вызывать метод GetCurrent() и MoveNext() сгенерированного перечислителя, последний из которых не является тривиальным (большая часть кода из GetAll была переместился туда) и использует довольно много переменных, сгенерированных компилятором.

Использование "Выход" обычно замедляет вашу программу, потому что компилятор должен создавать сложный код IL для представления конечного автомата.

Другие вопросы по тегам