Почему Seq.iter в 2 раза быстрее, чем для цикла, если цель для x64?

Отказ: это микро-бенчмарк, пожалуйста, не комментируйте цитаты, такие как "преждевременная оптимизация - это зло", если вы чувствуете недовольство по поводу темы.

Примерами являются релиз, предназначенный для x64,.Net4.5 Visual Studio 2012 F # 3.0 и запуск в Windows 7 x64

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

Наблюдение

Если в цикле for in или Seq.iter нет цикла, то ясно, что они имеют одинаковые скорости. (update2 vs update4)

Если внутри цикла for in или Seq.iter есть цикл, кажется, что Seq.iter равно 2x, как быстрее, чем for in. (обновление vs update3) странно? (если они будут работать в fsi, они будут похожи)

Если он предназначен для anycpu и запускается на x64, во времени нет разницы. Таким образом, вопрос становится следующим: Seq.iter(update3) увеличит скорость 2x, если цель равна x64

Время:

update:   00:00:11.4250483 // 2x as much as update3, why?
updatae2: 00:00:01.4447233
updatae3: 00:00:06.0863791
updatae4: 00:00:01.4939535

Исходный код:

open System.Diagnostics
open System

[<EntryPoint>]
let main argv = 
    let pool = seq {1 .. 1000000}

    let ret = Array.zeroCreate 100

    let update pool =
        for x in pool do
            for y in 1 .. 200 do
                ret.[2] <- x + y

    let update2 pool =
        for x in pool do
            //for y in 1 .. 100 do
                ret.[2] <- x


    let update3 pool =
        pool
            |> Seq.iter (fun x ->
                                  for y in 1 .. 200 do
                                      ret.[2] <- x + y)

    let update4 pool =
        pool
            |> Seq.iter (fun x ->
                                  //for y in 1 .. 100 do
                                      ret.[2] <- x)


    let test n =
        let run = match n with
                  | 1 -> update
                  | 2 -> update2
                  | 3 -> update3
                  | 4 -> update4
        for i in 1 .. 50 do
            run pool

    let sw = new Stopwatch()
    sw.Start()
    test(1)
    sw.Stop()
    Console.WriteLine(sw.Elapsed);

    sw.Restart()
    test(2)
    sw.Stop()
    Console.WriteLine(sw.Elapsed)

    sw.Restart()
    test(3)
    sw.Stop()
    Console.WriteLine(sw.Elapsed)

    sw.Restart()
    test(4)
    sw.Stop()
    Console.WriteLine(sw.Elapsed)
    0 // return an integer exit code

Ответ 1

Это не полный ответ, но надеюсь, что он поможет вам продвинуться дальше.

Я могу воспроизвести поведение, используя ту же конфигурацию. Вот более простой пример профилирования:

open System

let test1() =
    let ret = Array.zeroCreate 100
    let pool = {1 .. 1000000}    
    for x in pool do
        for _ in 1..50 do
            for y in 1..200 do
                ret.[2] <- x + y

let test2() =
    let ret = Array.zeroCreate 100
    let pool = {1 .. 1000000}    
    Seq.iter (fun x -> 
        for _ in 1..50 do
            for y in 1..200 do
                ret.[2] <- x + y) pool

let time f =
    let sw = new Diagnostics.Stopwatch()
    sw.Start()
    let result = f() 
    sw.Stop()
    Console.WriteLine(sw.Elapsed)
    result

[<EntryPoint>]
let main argv =
    time test1
    time test2
    0

В этом примере Seq.iter и for x in pool выполняется один раз, но между test1 и test2 все еще существует разница во времени:

00:00:06.9264843
00:00:03.6834886

Их ИЛ очень похожи, поэтому оптимизация компилятора не является проблемой. Похоже, что джиттер x64 не может оптимизировать test1, хотя он может сделать это с помощью test2. Интересно, что если я рефакторинг вложен в циклы в test1 как функцию, оптимизация JIT снова завершится:

let body (ret: _ []) x =
    for _ in 1..50 do
        for y in 1..200 do
            ret.[2] <- x + y

let test3() =
    let ret = Array.zeroCreate 100
    let pool = {1..1000000}    
    for x in pool do
        body ret x

// 00:00:03.7012302

Когда я отключу оптимизацию JIT с помощью описанной здесь техники , время выполнения этих функций сопоставимо.

Почему x64-джиттер не работает в конкретном примере, я не знаю. Вы можете дизассемблировать оптимизированный jitted-код для сравнения инструкций ASM по строкам. Может быть, кто-то с хорошим знанием ASM может узнать их отличия.

Ответ 2

Когда я запускаю эксперимент на своей машине (используя F # 3.0 в VS 2012 в режиме Release), я не получаю время, которое вы описываете. Вы постоянно получаете одинаковые номера при повторном запуске?

Я пробовал это примерно 4 раза, и я всегда получаю числа, которые очень похожи. Версия с Seq.iter имеет тенденцию быть немного быстрее, но это, вероятно, не является статистически значимым. Что-то вроде (используя Stopwatch):

test(1) = 15321ms
test(2) = 5149ms
test(3) = 14290ms
test(4) = 4999ms

Я запускаю тест на ноутбуке с Intel Core2 Duo (2.26Ghz), используя 64-битную Windows 7.