Производительность функций ограничения статического члена

Я пытаюсь изучить статические ограничения элементов в F #. От чтения сообщение в блоге Tomas Petricek, я понимаю, что запись функции inline, которая "использует только те операции, которые сами записываются с использованием статических ограничений для членов", сделает мою функцию корректной для всех числовых типов, которые удовлетворяют этим ограничениям. Этот вопрос указывает, что inline работает несколько аналогично шаблонам С++, поэтому я не ожидал разницы в производительности между этими двумя функциями:

let MultiplyTyped (A : double[,]) (B : double[,]) =
    let rA, cA = (Array2D.length1 A) - 1, (Array2D.length2 A) - 1
    let cB = (Array2D.length2 B) - 1
    let C = Array2D.zeroCreate<double> (Array2D.length1 A) (Array2D.length2 B)
    for i = 0 to rA do
        for k = 0 to cA do
            for j = 0 to cB do
                C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]
    C

let inline MultiplyGeneric (A : 'T[,]) (B : 'T[,]) =
    let rA, cA = Array2D.length1 A - 1, Array2D.length2 A - 1
    let cB = Array2D.length2 B - 1
    let C = Array2D.zeroCreate<'T> (Array2D.length1 A) (Array2D.length2 B)
    for i = 0 to rA do
        for k = 0 to cA do
            for j = 0 to cB do
                C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]
    C

Тем не менее, чтобы умножить две матрицы размером 1024 x 1024, MultiplyTyped завершается в среднем на 2550 мс на моей машине, тогда как MultiplyGeneric занимает около 5150 мс. Первоначально я думал, что zeroCreate был виноват в общей версии, но изменение этой строки на одну ниже не имело никакого значения.

let C = Array2D.init<'T> (Array2D.length1 A) (Array2D.length2 B) (fun i j -> LanguagePrimitives.GenericZero)

Что-то мне не хватает, чтобы сделать MultiplyGeneric так же, как MultiplyTyped? Или это ожидается?

изменить. Я должен упомянуть, что это VS2010, F # 2.0, Win7 64bit, релиз сборки. Цель платформы - x64 (для проверки больших матриц) - это имеет значение: x86 дает похожие результаты для двух функций.

Бонусный вопрос: тип, выводимый для MultiplyGeneric, следующий:

val inline MultiplyGeneric :
   ^T [,] ->  ^T [,] ->  ^T [,]
    when ( ^T or  ^a) : (static member ( + ) :  ^T *  ^a ->  ^T) and
          ^T : (static member ( * ) :  ^T *  ^T ->  ^a)

Откуда берется тип ^a?

изменить 2: здесь мой тестовый код:

let r = new System.Random()
let A = Array2D.init 1024 1024 (fun i j -> r.NextDouble())
let B = Array2D.init 1024 1024 (fun i j -> r.NextDouble())

let test f =
    let sw = System.Diagnostics.Stopwatch.StartNew()
    f() |> ignore
    sw.Stop()
    printfn "%A" sw.ElapsedMilliseconds

for i = 1 to 5 do
    test (fun () -> MultiplyTyped A B)

for i = 1 to 5 do
    test (fun () -> MultiplyGeneric A B)

Ответ 1

Я бы хотел увидеть ваши тесты. Я не получаю те же результаты (VS 2012 F # 3.0 Win 7 64-бит).

let m = Array2D.init 1024 1024 (fun i j -> float i * float j)

let test f =
  let sw = System.Diagnostics.Stopwatch.StartNew()
  f() |> ignore
  sw.Stop()
  printfn "%A" sw.Elapsed

test (fun () -> MultiplyTyped m m)
> 00:00:09.6013188

test (fun () -> MultiplyGeneric m m)
> 00:00:09.1686885

Декомпиляция с рефлектором, функции выглядят одинаково.

Что касается вашего последнего вопроса, выводится наименее ограничивающее ограничение. В этой строке

C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]

потому что тип результата A.[i,k] * B.[k,j] не указан и немедленно передается в (+), может быть задействован дополнительный тип. Если вы хотите затянуть ограничение, вы можете заменить эту строку на

let temp : 'T = A.[i,k] * B.[k,j]
C.[i,j] <- C.[i,j] + temp

Это изменит подпись на

val inline MultiplyGeneric :
  A: ^T [,] -> B: ^T [,] ->  ^T [,]
    when  ^T : (static member ( * ) :  ^T *  ^T ->  ^T) and
          ^T : (static member ( + ) :  ^T *  ^T ->  ^T)

ИЗМЕНИТЬ

Используя ваш тест, здесь вывод:

//MultiplyTyped
00:00:09.9904615
00:00:09.5489653
00:00:10.0562346
00:00:09.7023183
00:00:09.5123992
//MultiplyGeneric
00:00:09.1320273
00:00:08.8195283
00:00:08.8523408
00:00:09.2496603
00:00:09.2950196

Здесь тот же тест на ideone (с небольшими изменениями, чтобы оставаться в пределах срока: матрица 512x512 и одна итерация теста). Он запускает F # 2.0 и дает аналогичные результаты.

Ответ 2

Хороший вопрос. Сначала я отвечу на первую часть: ^a является частью процесса естественного обобщения. Представьте, что у вас был такой тип:
type T = | T with
    static member (+)(T, i:int) = T
    static member (*)(T, T) = 0

Затем вы можете использовать свою функцию MultiplyGeneric с массивами этого типа: умножение элементов A и B даст вам int s, но это нормально, потому что вы все равно можете добавить их к элементам C и вернуть значения типа T для возврата обратно в C.

Что касается вашего вопроса о производительности, я боюсь, что у меня нет отличного объяснения. Ваше основное понимание правильное - использование MultiplyGeneric с аргументами double[,] должно быть эквивалентно использованию MultiplyTyped. Если вы используете ildasm для поиска IL, компилятор генерирует следующий код F #:

let arr = Array2D.zeroCreate 1024 1024
let f1 = MultiplyTyped arr
let f2 = MultiplyGeneric arr

let timer = System.Diagnostics.Stopwatch()
timer.Start()

f1 arr |> ignore

printfn "%A" timer.Elapsed
timer.Restart()

f2 arr |> ignore

printfn "%A" timer.Elapsed

то вы можете видеть, что компилятор действительно генерирует идентичный код для каждого из них, вставляя встроенный код для MultipyGeneric во внутреннюю статическую функцию. Единственное отличие, которое я вижу в сгенерированном коде, - это имена локалей, а при запуске из командной строки я получаю примерно равные истекшие времена. Однако, работая с FSI, я вижу разницу, сходную с тем, что вы сообщили.

Мне непонятно, почему это было бы. Как я вижу, есть две возможности:

  • Генерация кода FSI может делать что-то немного отличное от статического компилятора
  • Компилятор CLR JIT может обрабатывать код, сгенерированный во время выполнения, несколько иначе, чем скомпилированный код. Например, как я уже упоминал, мой код выше, используя MultiplyGeneric, фактически приводит к внутреннему методу, содержащему встроенный элемент. Возможно, CLR JIT обрабатывает разницу между общедоступными и внутренними методами по-разному, когда они генерируются во время выполнения, чем когда они находятся в статически скомпилированном коде.