В Swift, как плохо объявить переменную в циклах

Я не знаю всех механиков Swift и как он обрабатывает переменные.

Я всегда предпочитал объявлять переменные перед тем, как вводить цикл for или while, а не язык, а не объявлять их внутри цикла снова и снова.

Но не так ли, чтобы повторно объявить переменные? Это повлияет на производительность с очень большой итерацией? Как конкретно Swift справляется с этим поведением?

пример :

while i < 100 {
  let a = someFunc()
  i += 1
}

В.С.

let a: MyObj
while i < 100 {
 a = someFunc()
 i += 1
}

Всего 2 ответа


Это не повлияет на производительность, и вариант 1 является очень предпочтительным. Даже если это повлияет на производительность, вам нужно будет продемонстрировать, что на вашем точном коде, прежде чем вы рассмотрите любой другой вариант, но версию 1. Нет универсальных ответов на производительность при работе с оптимизирующим компилятором. Выполнение чего-либо необычного «для производительности», которое вы еще не изучили с помощью своего кода, имеет высокую вероятность ухудшения ситуации. Обычными случаями являются наиболее оптимизированные случаи.

(Я знаю, что я преувеличиваю это. Есть определенные способы взглянуть на код и сказать: «это будет ужасно неэффективно». И есть некоторые причудливые части Swift, где все, что выглядит нормально, на самом деле плохое, особенно с использованием + для комбинирования строк или использования pre-Swift4 для создания массива. Но в тех случаях, когда это имеет значение, вы обнаружите это очень быстро, потому что они очень плохие, когда они имеют значение.)

Но мы не должны догадываться об этом. Мы можем просто спросить у компилятора.

// inside.swift
import Foundation

func runme() {
    var i = 0
    while i < 100 {
      let a = Int.random(in: 0...10)
      print(a)
      i += 1
    }
}


// outside.swift
import Foundation

func runme() {
    var i = 0
    var a: Int
    while i < 100 {
      a = Int.random(in: 0...10)
      print(a)
      i += 1
    }
}

Во-первых, обратите внимание, что я помещаю их в функцию. Это важно. Помещение их на верхний уровень делает глобальным в одном случае, а глобальные переменные имеют специальную обработку, в том числе инициализацию потокобезопасности, что делает «внешний» случай более дорогим и сложным, чем при более нормальном использовании. (Очень сложно правильно проверить микрооптимизацию таким образом, что вы можете сделать общие выводы «это быстрее». Существует так много факторов).

Во-вторых, обратите внимание на print . Мы должны убедиться, что вы используете a по-бок-эффектно, иначе оптимизатор может полностью удалить его. print довольно хороша, хотя это довольно сложно. Вы также можете использовать результат для изменения глобального, но компилятор может определенно оптимизировать это гораздо более агрессивно и может устранить вещи, которые мы хотели видеть. (Вам действительно нужно проверить этот материал на самом деле, о котором вы заботитесь.)

Теперь мы можем видеть, что Swift будет делать с каждым из них, используя swiftc -O -emit-sil . Это -O имеет решающее значение. Так много людей пытаются выполнить тестирование производительности без включения оптимизатора, и эти результаты не имеют смысла.

Итак, как выглядит SIL? (Swift Intermediate Language) Это первый большой шаг к превращению вашей программы в машинный код. Если две вещи генерируют один и тот же SIL, они собираются создать тот же машинный код.)

SIL немного длинный (8000 строк), поэтому я немного подкорректирую его. Мои комментарии в <>. Это будет немного утомительно, потому что изучение этого материала очень неуловимо. Если вы хотите пропустить это, TL-DR: нет никакой разницы между этими двумя частями кода. Не «небольшая разница, которая не имеет значения». Буквально (за исключением подсказки к отладчику), никакой разницы.

// runme()
sil hidden @$S4main5runmeyyF : $@convention(thin) () -> () {
bb0:
  ... <define a bunch of variables and function calls> ...

<compute the random number and put it in %29>
// %19                                            // user: %49
bb1(%19 : $Builtin.Int64):                        // Preds: bb5 bb0
  %20 = alloc_stack $SystemRandomNumberGenerator  // users: %23, %30, %21
  store %2 to %20 : $*SystemRandomNumberGenerator // id: %21
  br bb2                                          // id: %22

bb2:                                              // Preds: bb3 bb1
  %23 = apply %6<SystemRandomNumberGenerator>(%20, %5) : $@convention(method) <τ_0_0 where τ_0_0 : RandomNumberGenerator> (@inout τ_0_0, @thin UInt.Type) -> UInt // user: %24
  %24 = struct_extract %23 : $UInt, #UInt._value  // users: %28, %25
  %25 = builtin "cmp_ult_Int64"(%24 : $Builtin.Int64, %4 : $Builtin.Int64) : $Builtin.Int1 // user: %26
  cond_br %25, bb3, bb4                           // id: %26

bb3:                                              // Preds: bb2
  br bb2                                          // id: %27

bb4:                                              // Preds: bb2
  %28 = builtin "urem_Int64"(%24 : $Builtin.Int64, %3 : $Builtin.Int64) : $Builtin.Int64 // user: %29
  %29 = struct $Int (%28 : $Builtin.Int64)        // users: %42, %31
  dealloc_stack %20 : $*SystemRandomNumberGenerator // id: %30


< *** Note that %29 is called "a" *** >

  debug_value %29 : $Int, let, name "a"           // id: %31

... < The print call. This is a lot more code than you think it is...> ...

< Add one to i and check for overflow >

  %49 = builtin "sadd_with_overflow_Int64"(%19 : $Builtin.Int64, %8 : $Builtin.Int64, %13 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %51, %50
  %50 = tuple_extract %49 : $(Builtin.Int64, Builtin.Int1), 0 // users: %55, %53
  %51 = tuple_extract %49 : $(Builtin.Int64, Builtin.Int1), 1 // user: %52
  cond_fail %51 : $Builtin.Int1                   // id: %52


< Loop if i < 100 >
  %53 = builtin "cmp_slt_Int64"(%50 : $Builtin.Int64, %1 : $Builtin.Int64) : $Builtin.Int1 // user: %54
  cond_br %53, bb5, bb6                           // id: %54

bb5:                                              // Preds: bb4
  br bb1(%50 : $Builtin.Int64)                    // id: %55

bb6:                                              // Preds: bb4
  %56 = tuple ()                                  // user: %57
  return %56 : $()                                // id: %57
} // end sil function '$S4main5runmeyyF'

«Внешний» код почти идентичен. Что поделаешь? Обратите внимание, где *** в коде выше, обозначающем вызов debug_value ? Это отсутствует во «вне», потому что a определяется как функциональная переменная, а не блочная.

Знаете, чего не хватает в обоих этих? alloc_stack для «a». Это целое число; он может вписываться в регистр. Компилятор нижнего уровня зависит от того, хранится ли он в регистре или в стеке. Оптимизатор видит, что «а» не выходит из этой области кода, поэтому он включает в себя подсказку для отладчика, но на самом деле он не требует для него хранения, даже в стеке. Он может просто взять регистр возврата Random и перенести его в регистр параметров для print . Для LLVM и его оптимизатора это решить.

Урок из всего этого заключается в том, что это буквально не имеет значения для производительности. В неясных случаях, когда это может иметь значение (например, когда a является глобальным), версия 1 будет более эффективной, что, я полагаю, противоположно тому, что вы ожидали.


Swift обрабатывает это, как и большинство языков. Локальные переменные объявляются в стеке и удаляются из стека, когда вы выходите из области, в которой они определены. Нажатие и выталкивание стека - это очень недорогая операция.

Компилятор LLVM, который использует Swift, использует довольно продвинутую оптимизацию кода, особенно в режиме выпуска. В вашем тривиальном примере переменные, которые вы используете переменные, вполне могут быть оптимизированы в любом случае, поскольку они фактически не используются ни для чего.

РЕДАКТИРОВАТЬ:

Подводя итог, нет существенной разницы в производительности между 2, и первый подход к установке переменной внутри цикла является более чистым, как указано rmaddy в его комментарии. Определение переменных в самом узком возможном объеме - хорошая политика. Он показывает ваше намерение для переменной и позволяет избежать непреднамеренных последствий.


Есть идеи?

10000