如何在 F# 中优化这段代码的速度以及为什么一个部分执行两次?

How to optimize this code for speed, in F# and also why is a part executed twice?

该代码用于打包16个字节的历史财务数据:

type PackedCandle =
    struct
        val H: single
        val L: single
        val C: single
        val V: int
    end
    new(h: single, l: single, c: single, v: int) = { H = h; L = l; C = c; V = v }
    member this.ToByteArray =
        let a = Array.create 16 (byte 0)
        let h = BitConverter.GetBytes(this.H)
        let l = BitConverter.GetBytes(this.L)
        let c = BitConverter.GetBytes(this.C)
        let v = BitConverter.GetBytes(this.V)
        a.[00] <- h.[0]; a.[01] <- h.[1]; a.[02] <- h.[2]; a.[03] <- h.[3]
        a.[04] <- l.[0]; a.[05] <- l.[1]; a.[06] <- l.[2]; a.[07] <- l.[3]
        a.[08] <- c.[0]; a.[09] <- c.[1]; a.[10] <- c.[2]; a.[11] <- c.[3]
        a.[12] <- v.[0]; a.[13] <- v.[1]; a.[14] <- v.[2]; a.[15] <- v.[3]
        printfn "!!" <- for the second part of the question
        a

这些数组通过网络发送,因此我需要数据尽可能小,但由于这是同时跟踪大约 80 种可交易工具,因此性能也很重要。 在客户端不获取历史数据然后更新的情况下进行权衡,但只是逐分钟获取最近 3 天的数据块,导致一遍又一遍地发送相同的数据以简化客户端逻辑..我继承了这个问题使低效的设计..尽可能高效。这也是通过休息轮询完成的,我现在正在将其转换为套接字以保持所有二进制文件。

所以我的第一个问题是: 我怎样才能让它更快?在 C 中,你可以将任何东西转换成任何东西,我可以只取一个浮点数并将它直接写入数组,所以没有什么比这更快的了,但在 F# 中,看起来我需要跳过箍,获取字节,然后将它们复制一个一个而不是 4 乘 4,等等。有更好的方法吗?

我的第二个问题是,由于要计算一次,所以我将 ToByteArray 设置为 属性。我正在 Jupyter Notebook 中使用随机值进行一些测试,但随后我看到:

属性 似乎被执行了两次(由两个“!!”行表示)。这是为什么?

我的第一个问题是,为什么需要 ToByteArray 操作?在评论中,您说您正在通过网络发送这些值的数组,所以我假设您计划将数据转换为字节数组,以便您可以将其写入网络流。

我认为使用一种采用 StreamWriter 并将数据直接写入流的方法会更有效(也更容易):

type PackedCandle =
  struct
      val H: single
      val L: single
      val C: single
      val V: int
  end
  new(h: single, l: single, c: single, v: int) = { H = h; L = l; C = c; V = v }
  member this.WriteTo(sw:StreamWriter) =
      sw.Write(this.H)
      sw.Write(this.L)
      sw.Write(this.C)
      sw.Write(this.V)

如果您现在有一些用于网络通信的代码,这将公开一个流,您将需要写入该流。假设这是 stream,你可以这样做:

use writer = new StreamWriter(stream)
for a in packedCandles do a.WriteTo(writer)

关于你的第二个问题,我认为没有更完整的代码示例无法回答。

假设您有要写入的数组(通常在使用套接字时您应该使用缓冲区进行读写),您可以使用 System.Runtime.CompilerServices.Unsafe.As<TFrom, TTo> 将内存从一种类型转换为另一种类型(同样的事情你可以用 C/C++)

type PackedCandle =
    // omitting fields & consructor
    override c.ToString() = $"%f{c.H} %f{c.L} %f{c.C} %d{c.V}" // debug purpose

    static member ReadFrom(array: byte[], offset) =
        // get managed(!) pointer
        // cast pointer to another type
        // same as *(PackedCandle*)(&array[offset]) but safe from GC
        Unsafe.As<byte, PackedCandle> &array.[offset]

    member c.WriteTo(array: byte[], offset: int) =
        Unsafe.As<byte, PackedCandle> &array.[offset] <- c

用法

let byteArray = Array.zeroCreate<byte> 100 // assume array come from different function

// writing
let mutable offset = 0
for i = 0 to 5 do
    let candle = PackedCandle(float32 i, float32 i, float32 i, i)
    candle.WriteTo(byteArray, offset)
    offset <- offset + Unsafe.SizeOf<PackedCandle>() // "increment pointer"

// reading
let mutable offset = 0
for i = 0 to 5 do
    let candle = PackedCandle.ReadFrom(byteArray, offset)
    printfn "%O" candle
    offset <- offset + Unsafe.SizeOf<PackedCandle>()

但是你真的想弄乱指针(甚至管理)吗?测出这段代码是瓶颈?

更新

最好在运行时使用 MemoryMarshal instead of raw Unsafe because first checks out-of-range and enforces usage of unmanaged (see here or here) 类型

member c.WriteTo (array: byte[], offset: int) =
    MemoryMarshal.Write(array.AsSpan(offset), &Unsafe.AsRef(&c))

static member ReadFrom (array: byte[], offset: int) =
    MemoryMarshal.Read<PackedCandle>(ReadOnlySpan(array).Slice(offset))