Перейти к содержимому
MrAbad

Оптимизация кода

Рекомендуемые сообщения

Решил я как-то поискать методы оптимизации кода для ОпенКомпов, и Луа в целом. Но на форуме, вроде, я не нашел тему, где в одном месте сконцентрированы вещи, помогающие в оптимизации. Разве что нашел одну ветку, на заморском форуме, вот ее компиляция/перевод/адаптация/дополнение:
(за помощь в переводе, спасибо KoshakLol и Google Translate)
 

# Место в памяти

Вот таблица, где есть, сколько что занимает в памяти:
boolean, number и string занимают 9 байт.
Указатель на функцию, или на таблицу, тоже 9 байт.
Пустая функция (function() end) занимает 93 байт, пустая функция с return (function() return true end) занимает 102 байт.
Пустая таблица занимает 36 байт, плюс за каждую степень двойки (2^n) еще дополнительно:

  • 9 байт для 0-1 элементов в таблице
  • 18 байт для 2 элементов в таблице
  • 36 байт для 3-4 элементов в таблице
  • 71 байт для 5-8 элементов в таблице
  • 142 байт для 9-16 элементов в таблице
  • 284 байт для 17-32 элементов в таблице
  • 569 байт для 33-64 элементов в таблице

и т.д.
И, кстати, таблицы не сжимаются, то есть если туда запихать 1000 элементов, а потом удалить их, то весить она будет, как будто в ней все еще 1000 элементов
 

# Сборка мусора


Автоматический сборщик мусора в опенкомпах вроде бы отключен, и вызывается только через os.sleep(0), и то не гарантированно, так что разработчик мода советует его вызывать 10 раз подряд, чтоб уж точно
И еще, автоматический сборщик чистит ТОЛЬКО локальные переменные


# Советы сомнительной эффективности (от них я не засек прироста)


Убрать из кода деление, потому что оно, якобы, медленнее, чем умножение (условно:)

local a = n / 4
-->
local a = n * 0.25

Заменить все условные операторы, на логические тернарные операторы, где это возможно (условно:)

if (a > b) then
    c = a
else
    c = b
end
-->
c = (a > b) and a or b

(Если вы не поняли, что сейчас вообще было, то в этой теме все расписано)

Менять переменные местами, без буфера (Lua 5.3+)

local a = 174
local b = 3
a = a ~ b
b = a ~ b
a = a ~ b
print(a)
--> 3
print(b)
--> 174

(Если вы не поняли, что сейчас вообще было, то ищите битовую операцию XOR)
Этот совет дает хотя бы небольшой прирост, из-за того, что нет лишний переменной, но этот плюс нивелируется, если ее удалить


# В итоге


Не используйте рекурсию функций, а если совсем приспичило, то почаще вставляйте сбор мусора
Не используйте замыкания (это когда функция возвращает другую функцию)
Старайтесь не использовать таблицы, вместо них лучше делать так:

local coords = {x = 10, y = 20, z = 370} -- (72 байта)
-->
local x = 10
local y = 20
local z = 370 -- (27 байт)
-- или вообще
local xyz = 010020370 -- (9 байт)
print("z = ", xyz // 1000000, "\ny = " , xyz // 1000 % 1000, "\nz = ", xyz % 1000)
-- (Если вы не поняли, что сейчас вообще было, то ищите деление с остатком)

Старайтесь не использовать не-локальные переменные, так как они бьют по архитектуре программы, и их не собирает гарбадж коллектор

# P.S

В данной заметке рассматривалась только сторона оптимизации кода, определенный код может отвратительно выглядеть, но работать быстро, и наоборот, в реальных программах лучше соблюдать баланс, между красотой, и быстродействием, в какой-нибудь программе для EEPROM'a, допустим, вообще о красоте кода не идет и речи
Ну это в целом все, что я хотел рассказать, если есть исправления/уточнения/дополнения/свои_идеи, то милости прошу в комментарии

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
3 часа назад, MrAbad сказал:

Менять переменные местами, без буфера (Lua 5.3+)


local a = 174
local b = 3
a = a ~ b
b = a ~ b
a = a ~ b
print(a)
--> 3
print(b)
--> 174

 

А как на счет такого варианта?

a,b = b,a

На сколько это более/менее производительней чем пример с XORом? Во всяком случае это на много понятней и занимает меньше букв, что существенно, если программа лежит в EEPROM.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
8 часов назад, Zer0Galaxy сказал:

А как на счет такого варианта?


a,b = b,a

На сколько это более/менее производительней чем пример с XORом? Во всяком случае это на много понятней и занимает меньше букв, что существенно, если программа лежит в EEPROM.

В Lua эта конструкция создает буферную переменную

a, b = b, a
-->
local с = a
a = b
b = c

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
12 часа назад, MrAbad сказал:

Не используйте замыкания (это когда функция возвращает другую функцию)

Почему?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
Только что, hohserg сказал:

Почему?

ну во-первых можно обойтись и без них, во-вторых просто оболочка функции занимает 1/10 килобайта, то есть просто замыкание, которое ничего не делает использует 2/10 килобайта. Согласен, что в некоторых ситуациях они могут быть полезны, но в том же пресловутом EEPROM'е не до красивой архитектуры с функциональным программированием

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Если кратко у любителей факторки поймал вот эту книжку.

Написал вот этот код:

Скрытый текст

local computer = require("computer")

function freeMemory()
  local result = 0
  for i = 1, 10 do
    result = math.max(result, computer.freeMemory())
    os.sleep(0)
  end
  return result
end
function memoize (f)
local mem = {} -- memoizing table
setmetatable(mem, {__mode = "kv"}) -- make it weak
return function (x) -- new version of ’f’, with memoizing
local r = mem[x]
if r == nil then -- no previous result?
r = f(x) -- calls original function
mem[x] = r -- store result for reuse
end
return r
end
end


print("function")
local start = os.clock()
for i=1,100000 do
  local x = math.sin(i) --0.026591985
end
print(os.clock()-start)

start = os.clock()
local sin = math.sin
for i=1,100000 do
  --local x = math.sin(i) --0.026591985
  local x = sin(i)        --0.01030093
end
print(os.clock()-start)

print("tables")
start = os.clock()
for i = 1, 100000 do 
  local a = {}
  a[1]=1; a[2] = 2; a[3] = 3
end
print(os.clock()-start)

start = os.clock()
for i = 1, 100000 do 
  local a = {true,true,true}
  a[1]=1; a[2] = 2; a[3] = 3
end
print(os.clock()-start)

print("fill nil")
t = {}
lim = 20000
for i = 1, lim do t[i] = i end

start = os.clock()
for k in pairs(t) do
  t[k] = nil
end
print(os.clock()-start)

t = {}
for i = 1, lim do t[i] = i end

start = os.clock()
while true do -- хорош для малых таблиц и поиска по ним. Но крашит комп при больших таблицах
  local k = next(t)
  if not k then break end
  t[k] = nil
end
print(os.clock()-start)

print("time demo")
start = os.clock()
local t = {}
for i = 1970, 2000 do
t[i] = os.time({year = i, month = 6, day = 14})
end
print(os.clock()-start)

start = os.clock()
local t = {}
local aux = {year = nil, month = 6, day = 14}
for i = 1970, 2000 do
aux.year = i
t[i] = os.time(aux)
end
print(os.clock()-start)

print("memory demo")
print("start \t \t",computer.freeMemory())
freeMemory()
for i = 1, lim do t[i] = i end
print("with normal table ", computer.freeMemory())
t_m = memoize(t)
t = {}
print("with memoryzed \t",computer.freeMemory())




 

И вот какой результат:

0w-W8BFTGkkERaIfJR-k4cx8_ImaI-SJhBxVCfaR
Как видно некоторые моменты реально спасают а некоторые не очень. 
Там еще есть совет о корутинах но я не проверял.
 

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Изначально я не хотел комментировать этот «урок», т.к. объём комментариев в несколько раз превысил бы изначальный текст. Главная претензия к нему в отсутствии конкретики и каких-либо примеров, позволяющих читателю прийти к тем же выводам.

 

@Taruu , приведя конкретный пример, спасает этот урок.

 

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

 

Демонстрация: локальная функция sin работает в два раза быстрее, чем функция math.sin. Замедление происходит два раза: при извлечении поля math из таблицы глобальных переменных и при извлечении поля sin из таблицы math.

кусочек кода

Также можно привести ещё пример, демонстрирующий, что работа с глобальной переменной равносильна работе с полем таблицы.

 

А ещё важно продемонстрировать, начиная с какого числа использований становится выгодным копировать функцию math.sin в локальную переменную. Потому что если бездумно повторять советы из этого «урока», то вместо оптимизации может получиться пессимизация.

 

Также я советую для записи чисел с большим количеством нулей использовать научную форму. Чтобы понять, сколько нулей содержит число 100000, мне приходится задерживать взгляд на этом числе, а запись 1e5 считывается мгновенно.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
2 часа назад, eu_tomat сказал:

Изначально я не хотел комментировать этот «урок», т.к. объём комментариев в несколько раз превысил бы изначальный текст. Главная претензия к нему в отсутствии конкретики и каких-либо примеров, позволяющих читателю прийти к тем же выводам.

 

@Taruu , приведя конкретный пример, спасает этот урок.

 

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

 

Демонстрация: локальная функция sin работает в два раза быстрее, чем функция math.sin. Замедление происходит два раза: при извлечении поля math из таблицы глобальных переменных и при извлечении поля sin из таблицы math.


кусочек кода

Также можно привести ещё пример, демонстрирующий, что работа с глобальной переменной равносильна работе с полем таблицы.

 

А ещё важно продемонстрировать, начиная с какого числа использований становится выгодным копировать функцию math.sin в локальную переменную. Потому что если бездумно повторять советы из этого «урока», то вместо оптимизации может получиться пессимизация.

 

Также я советую для записи чисел с большим количеством нулей использовать научную форму. Чтобы понять, сколько нулей содержит число 100000, мне приходится задерживать взгляд на этом числе, а запись 1e5 считывается мгновенно.

Я больше как пример того что смог найти. Оформлять это точно еще надо (я просто протестировал наскоряк).

Ихмо я бы залез на уровень языка и посмотрел почему это работает быстрее :/
Сейчас попробую оформить это хоть во что-то читабельное. 

Изменено пользователем Taruu

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Итак делаю более структурированный пост чем был до этого...

 

Для показания производительности используются:

os.clock() - дает приблизительное время работы CPU во время исполнения программы.

computer.freeMemory() - дает нам узнать сколько памяти есть у компа.

 

Локализация функций:

Делайте функции в программе локальными. Это работает как и с локализацией существующих функций.

Пример с функцией math.sin и ее локальной версией:

local computer = require("computer")
local unicode = require("unicode")
local x = 0
local sin = math.sin
local start = os.clock()
local memory_start = 0
for i=1,1e5 do
  x = math.sin(i)
end
print("1e5 math.sin",os.clock()-start)
start = os.clock()

for i=1,1e5 do
  x = sin(i)
end
print("1e5 local.sin",os.clock()-start)

--Замер для редкого вызова
start = os.clock()
x = math.sin(3)
x = math.sin(10)
x = math.sin(16)
x = math.sin(42)
print("rare math.sin",os.clock()-start)


start = os.clock()
x = sin(3)
x = sin(10)
x = sin(16)
x = sin(42)
print("rare local.sin",os.clock()-start)

--Замер для одиночного вызова 
memory_start = computer.freeMemory() -- проверка на кушание памяти
start = os.clock()
x = math.sin(42)
print("one math.sin",os.clock()-start, "memory take", memory_start-computer.freeMemory())

x = 0

memory_start = computer.freeMemory()
local sin2 = math.sin
start = os.clock()
x = sin2(42)
print("one local.sin2",os.clock()-start, "memory take",memory_start-computer.freeMemory())

image.png.22d7dba130365843bad6cbd0523bd34a.png

 

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

Так же я решил проверить данный метод на openOS библиотеке unicode

local computer = require("computer")
local unicode = require("unicode")
local x = 0
local start = os.clock()
local memory_start = 0
local un_len = unicode.len

for i=1,1e5 do
  x = unicode.len(tostring(i))
end
print("1e5 unicode.len",os.clock()-start)

start = os.clock()

for i=1,1e5 do
  x = un_len(i)
end
print("1e5 local.len",os.clock()-start)

--Замер для редкого вызова
start = os.clock()
x = unicode.len("Арбуз")
x = unicode.len("Помидор")
x = unicode.len("Капитализм")
x = unicode.len("Ракета")
print("rare unicode.len",os.clock()-start)


start = os.clock()
x = un_len("Арбуз")
x = un_len("Помидор")
x = un_len("Капитализм")
x = un_len("Ракета")
print("rare local.len",os.clock()-start)

--Замер для одиночного вызова 
memory_start = computer.freeMemory() -- проверка на кушание памяти
start = os.clock()
x = unicode.len("Hello world!")
print("one unicode.len",os.clock()-start, "memory take", memory_start-computer.freeMemory())

x = 0

memory_start = computer.freeMemory()
local un_len2 = unicode.len
start = os.clock()
x = un_len2("Hello world!")
print("one local.len",os.clock()-start, "memory take",memory_start-computer.freeMemory())

image.png.697751ec99d183a314e031bc3df1a15e.png

Тут же прирост в производительности не особо заметен. Так что стоит отдельно проверять библиотеку или функцию на эффективность такой локализации.

 

Так же можно локализовать и свои функции:

local computer = require("computer")
local x = 0
local start = 0 
local memory_start = 0

memory_start = computer.freeMemory()
function func1(a,b)
  return a + b
end
start = os.clock()
x = func1(9,0.3)
print("norm func",os.clock()-start, "memory take",memory_start-computer.freeMemory())


memory_start = computer.freeMemory()
local func2 = function(a,b)
return a + b
end
start = os.clock()
x = func2(9,0.3)
print("local func",os.clock()-start, "memory take",memory_start-computer.freeMemory())

start = os.clock()
for i=1,1e5 do
  local x = func1(i,i)
end
print("func1 \t",os.clock()-start)

start = os.clock()
local sin = math.sin
for i=1,1e5 do
  local x = func2(i,i)
end
print("func2 \t",os.clock()-start)

image.png.ea16aaa1e082ecc93f0e02a544f315e3.png

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

 

 

Локальные переменные

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

--var_global
local computer = require("computer")
local memory_start = computer.freeMemory()
local test_table = {1,2,3,4,5,6,7,8,9,10}
function work_on_test_table()
--Что то делаем с test_table
end
print("memory take", memory_start - computer.freeMemory())

--var_local
local computer = require("computer")
local memory_start = computer.freeMemory()
function work_on_test_table()
local test_table = {1,2,3,4,5,6,7,8,9,10}
--Что то делаем с test_table
end
print("memory take", memory_start - computer.freeMemory())

4w4POAPIln_nz-C1Gkxs6Rb3GtVO0VVNWIeknF3s

 

Подготовка объектов (таблиц)

Лучше подготовить таблицу с частью необходимых значений и их уже дополнять.

print("create obj")
start = os.clock()

local t = {}
for i = 1970, 2000 do
  t[i] = os.time({year = i, month = 6, day = 14})
end

print("direct insertion time", os.clock()-start)

start = os.clock()
t = {}
local aux = {year = nil, month = 6, day = 14}
for i = 1970, 2000 do
  aux.year = i
  t[i] = os.time(aux)
end

print("prepared insert time", os.clock()-start)



start = os.clock()
for i = 1, 1e5 do 
  local a = {}
  a[1]=1; a[2] = 2; a[3] = 3
end
print("num insert", os.clock()-start)

start = os.clock()
for i = 1, 1e5 do 
  local a = {true,true,true}
  a[1]=1; a[2] = 2; a[3] = 3
end
print("prepared insert", os.clock()-start)

tPC1ihHgURvxoQsevGldwE60RygYknJXwjem5z9P

 

 

X*X вместо X^2

Если нам нужна 2 степень числа то быстрее это будет умножить чем возводить в степень, Но если у нас уже 3 степень или больше то данный финт бесполезен.


local start = os.clock()
for i=1,1e7 do
  local x = i * i
end
print("*",os.clock()-start)

start = os.clock()
for i=1,1e7 do 
  local x = i ^ 2
end
print("^",os.clock()-start)

start = os.clock()
for i=1,1e7 do
  local x = i * i * i * i * i
end
print("* X5",os.clock()-start)
start = os.clock()
for i=1,1e7 do 
  local x = i ^ 5
end
print("^ 5",os.clock()-start)

image.png.17126800d285174693f3b9d502666173.png

 

Swap переменных

Самый быстрый способ это использовать временную переменную,

красивый и почти такой же эффективный метод перестановки переменных будет a,b = b,a

Так же стоит отметить что не один из перечисленных способов не требует выделения памяти.

Скрытый текст

local a,b = 23,76

local computer = require("computer")
local memory_start = 0
local start = 0

print("test 1e7")
memory_start = computer.freeMemory()
start = os.clock()
for i=1,1e7 do
  a,b=b,a
end
print("swap a,b = b,a", os.clock()-start, memory_start - computer.freeMemory())  -- Результат: 0

memory_start = computer.freeMemory()
start = os.clock()
local temp1 = 0
for i=1,1e7 do
  temp1 = a
  a = b
  b = temp1
end
print("temp \t",os.clock()-start, memory_start - computer.freeMemory()) 

memory_start = computer.freeMemory()
start = os.clock()
for i=1,1e7 do
  a=a~b  
  b=a~b
  a=a~b   
end
print("swap ~ \t",os.clock()-start, memory_start - computer.freeMemory()) 

print()
print("test 100")
memory_start = computer.freeMemory()
start = os.clock()
for i=1,100 do
  a,b=b,a
end
print("swap a,b = b,a", os.clock()-start, memory_start - computer.freeMemory())  -- Результат: 0

memory_start = computer.freeMemory()
start = os.clock()
local temp2 = 0
for i=1,100 do
  temp2 = a
  a = b
  b = temp2
end
print("swap temp",os.clock()-start, memory_start - computer.freeMemory()) 

memory_start = computer.freeMemory()
start = os.clock()
for i=1,100 do
  a=a~b  
  b=a~b
  a=a~b   
end
print("swap ~ \t",os.clock()-start, memory_start - computer.freeMemory()) 

print()
print("one swap")
memory_start = computer.freeMemory()
start = os.clock()
a,b=b,a
print("swap a,b=b,a ",os.clock()-start, memory_start - computer.freeMemory()) 

memory_start = computer.freeMemory()
start = os.clock()
local temp = 0
temp = a
a = b
b = temp
print("swap temp",os.clock()-start, memory_start - computer.freeMemory()) 

memory_start = computer.freeMemory()
start = os.clock()
a=a~b 
b=a~b
a=a~b
print("swap ~ \t",os.clock()-start, memory_start - computer.freeMemory()) 

 

image.png.0e166306086cc5bd86a482ecc8e3a2ff.png

 

 

 

Вот все что смог более менее конкретное накопать и проверить. Прошу кинуть в лицо ошибки или вопросы. Вместе подумаем.... Теперь поправили.

image.png

Изменено пользователем Taruu
Внесли правки по некоторым темам

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Пффф.. Ну, раз я влез в эту тему, то продолжу обсуждение.

 

1 час назад, Taruu сказал:

os.clock() - показывает как долго запущена программа

Не совсем. Как долго работает компьютер, скажет computer.uptime(), а os.clock() скажет, сколько из этого времени компьютер потратил на, собственно, вычисления. Конечно, с некоторыми оговорками, но в целом именно os.clock() позволяет оценить нагрузку, создаваемую тем или иным кодом на игровой сервер.

 

2 часа назад, Taruu сказал:

Делайте функции в программе локальными. Это работает как и с локализацией существующих функций:

Пример с функцией math.sin и ее локальной версией:

...

Как видно мы получили очень большой прирост в производительности. Почти в два раза! Что уже неплохо.
Так же это особенность работает и на своих функциях.

Это грязный эксперимент с неправильной интерпретацией результата. В нём извлечение поля из таблицы выполняется два раза, о чём не упомянуто в описании.

 

Также эксперимент не сообщает о том, имеет ли смысл создавать локальную переменную для функции math.sin, если её планируется использовать всего два раза. Или пять, например.

 

2 часа назад, Taruu сказал:

X*X вместо X^2

...

Co7XNPFeeJMu3v1-R2rcgjH0BcDez0hXxtaxmTFq

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

 

2 часа назад, Taruu сказал:

Самый быстрый способ это создать временную переименую и записать одну меняемую переименую в нее.

А здесь как раз именно шум не позволяет увидеть, что a,b = b,a имеет ровно ту же эффективность. Вообще, при интерпретации результатов следует сравнивать полученную разницу с шумовой, чтобы не делать преждевременных выводов.

 

2 часа назад, Taruu сказал:

Если же мы сильно экономим память то нам отлично подойдет побитовый XOR (~)

В этом месте я всё-таки ожидал увидеть экспериментальное подтверждение «эффективности» этого трюка. Но именно это заблуждение надо развеять в первую очередь.

Эксперимент:

local a,b = 23,76

local computer = require("computer")
local mem0 = computer.freeMemory()

for i=1,1e7 do
  a,b=b,a
end
print(mem0 - computer.freeMemory())  -- Результат: 0

for i=1,1e7 do
  a=a~b  
  b=a~b
  a=a~b   
end
print(mem0 - computer.freeMemory())  -- Результат: 6631

Результат: Обмен значений переменных через a,b=b,a потребляет ровно 0 дополнительной памяти и быстро работает. Зато обмен посредством XOR и память потребляет, и работает медленно, да ещё и код загромождает.

 

Я не знаю, почему этот вредный совет транслируют на форумах программистов. Вне сомнений, в арсенале программиста должен быть трюк обмена переменных через исключающее или. Но зачем его тулить к месту и не к месту? Урок же вроде как про оптимизацию кода.

 

И даже если бы этот трюк оказался эффективным, почему нет упоминания о том, что xor работает не для всех типов переменных?

 

И раз пошёл такой разговор, разберу предыдущее обсуждение:

В 01.04.2021 в 09:29, Zer0Galaxy сказал:

А как на счет такого варианта?


a,b = b,a

На сколько это более/менее производительней чем пример с XORом?

Эффективнее со всех точек зрения.

В 01.04.2021 в 17:50, MrAbad сказал:

В Lua эта конструкция создает буферную переменную

Lua для любых операций создаёт буферные переменные для хранения временных результатов. Но, в случае a,b=b,a они уничтожаются незамедлительно в отличие от xor. Операция a,b=b,aтребует на выполнение столько же времени, как три отдельных операции присваивания, но потребления памяти не увеличивает.

 

 

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
Скрытый текст
7 часов назад, eu_tomat сказал:

Пффф.. Ну, раз я влез в эту тему, то продолжу обсуждение.

 

Не совсем. Как долго работает компьютер, скажет computer.uptime(), а os.clock() скажет, сколько из этого времени компьютер потратил на, собственно, вычисления. Конечно, с некоторыми оговорками, но в целом именно os.clock() позволяет оценить нагрузку, создаваемую тем или иным кодом на игровой сервер.

 

Это грязный эксперимент с неправильной интерпретацией результата. В нём извлечение поля из таблицы выполняется два раза, о чём не упомянуто в описании.

 

Также эксперимент не сообщает о том, имеет ли смысл создавать локальную переменную для функции math.sin, если её планируется использовать всего два раза. Или пять, например.

 

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

 

А здесь как раз именно шум не позволяет увидеть, что a,b = b,a имеет ровно ту же эффективность. Вообще, при интерпретации результатов следует сравнивать полученную разницу с шумовой, чтобы не делать преждевременных выводов.

 

В этом месте я всё-таки ожидал увидеть экспериментальное подтверждение «эффективности» этого трюка. Но именно это заблуждение надо развеять в первую очередь.

Эксперимент:



local a,b = 23,76

local computer = require("computer")
local mem0 = computer.freeMemory()

for i=1,1e7 do
  a,b=b,a
end
print(mem0 - computer.freeMemory())  -- Результат: 0

for i=1,1e7 do
  a=a~b  
  b=a~b
  a=a~b   
end
print(mem0 - computer.freeMemory())  -- Результат: 6631

Результат: Обмен значений переменных через a,b=b,a потребляет ровно 0 дополнительной памяти и быстро работает. Зато обмен посредством XOR и память потребляет, и работает медленно, да ещё и код загромождает.

 

Я не знаю, почему этот вредный совет транслируют на форумах программистов. Вне сомнений, в арсенале программиста должен быть трюк обмена переменных через исключающее или. Но зачем его тулить к месту и не к месту? Урок же вроде как про оптимизацию кода.

 

И даже если бы этот трюк оказался эффективным, почему нет упоминания о том, что xor работает не для всех типов переменных?

 

И раз пошёл такой разговор, разберу предыдущее обсуждение:

Эффективнее со всех точек зрения.

Lua для любых операций создаёт буферные переменные для хранения временных результатов. Но, в случае a,b=b,a они уничтожаются незамедлительно в отличие от xor. Операция a,b=b,aтребует на выполнение столько же времени, как три отдельных операции присваивания, но потребления памяти не увеличивает.

 

 

 

Сейчас поправим тогда.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

CKmOLVg.png

Изменено пользователем LeshaInc

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
9 часов назад, eu_tomat сказал:

В этом месте я всё-таки ожидал увидеть экспериментальное подтверждение «эффективности» этого трюка. Но именно это заблуждение надо развеять в первую очередь.

Эксперимент:


local a,b = 23,76

local computer = require("computer")
local mem0 = computer.freeMemory()

for i=1,1e7 do
  a,b=b,a
end
print(mem0 - computer.freeMemory())  -- Результат: 0

for i=1,1e7 do
  a=a~b  
  b=a~b
  a=a~b   
end
print(mem0 - computer.freeMemory())  -- Результат: 6631

 

 

 

Тут ошибочка получается. У тебя print в памяти остается ?:/ Так что каждый раз нужно в переменную записывать.

local a,b = 23,76

local computer = require("computer")
local mem0 = computer.freeMemory()

for i=1,1e7 do
  a,b=b,a
end
print(mem0 - computer.freeMemory())  -- Результат: 0
mem0 = computer.freeMemory()
for i=1,1e7 do
  a=a~b  
  b=a~b
  a=a~b   
end
print(mem0 - computer.freeMemory()) 
print()
mem0 = computer.freeMemory()
print("test")
print(mem0 - computer.freeMemory()) 

И вот что мне вывело:

image.png.9105b53b7cd04a052b325b78f62e1dc7.png

 

Изменено пользователем Taruu

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
5 минут назад, Taruu сказал:

Тут ошибочка получается. У тебя print в памяти остается ?:/ Так что каждый раз нужно в переименую записывать.

Да, здесь я ошибся. Обе операции не потребляют дополнительную память, но xor всё равно хуже по другим причинам.

 

@LeshaInc своей картинкой, видимо, намекает на сложность темы, но это сразу было понятно. Тесты, измеряющие код, всегда сложно создавать, потому как кодомерка сама может вносить погрешность.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Осталось лишь решить проблему шума. Интервалы времени длительностью 1.6629999990414e-06 вообще ни о чём не говорят на фоне шума измерений. Надо повторять эксперимент в цикле с достаточным количеством итераций.

 

Например, для проверки одноразового вызова math.sin я бы использовал такие циклы:

for i=1,1e6 do
  local sin = math.sin
  x = sin(3)
end

for i=1,1e6 do
  x = math.sin(3)
end

Но этого тоже недостаточно. Есть разброс значений. Поэтому надо повторять эксперимент какое-то количество раз. Какое именно? Надо вспоминать основы статистики. И если у кого-то эти воспоминания достаточно свежи, дайте совет.

 

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

 

Сервер нагружен неравномерно. И для выравнивания замеров имеет смысл чередовать циклы экспериментов: Выполнили в цикле один код, сделали короткую паузу, выполнили цикл со вторым кодом, сделал паузу, выполнили цикл с третим, и т.д., по кругу.

 

Сервер бывает нагружен настолько неравномерно, что некоторые замеры аномально отличаются от среднего значения в болшьую сторону, и их надо бы отбрасывать. Мы же не характеристики сервера измеряем, а сравниваем эффективность кода. Но тут снова нужно аккуратно применять статистику.

 

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

 

Один я не готов оценивать глубину этой кроличьей норы, но @Taruu, мы можем попробовать написать такую библиотеку вместе. Также я прошу откликнуться и других форумчан. В тонкой сфере измерений аудит кода никогда не будет лишним. Также нужны идеи. Возможно, всё можно сделать проще, чем мне кажется.

 

А я попробую в ближайшие дни найти все старые наработки и скомпилировать их во что-то определённое.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

@eu_tomat Ок. Глянь что у тебя было в наработках. Посмотрим что было и что можно придумать. А так я хз стоит ли лезть так глубоко. Ибо может оказаться что результат будет тот же.

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
В 10.04.2021 в 17:16, eu_tomat сказал:

Изначально я не хотел комментировать этот «урок», т.к. объём комментариев в несколько раз превысил бы изначальный текст. Главная претензия к нему в отсутствии конкретики и каких-либо примеров, позволяющих читателю прийти к тем же выводам.

не отрицаю, что поверхностно, это в основном просто перевод заметки с заморского форума, в будущем постараюсь что-то по-глубже делать
В конце концов моя цель частично достигнута, есть тема, где есть кладесь информации, по-оптимизации кода

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах
3 часа назад, MrAbad сказал:

есть тема, где есть кладесь информации, по-оптимизации кода

Это далеко не кладезь. Синтетические тесты, конечно, полезны для понимания, но их практическая ценность не высока. Нужны какие-то примеры, приближенные к реальности: программа, долго считающая что-то нужное, которая после оптимизации начинает считать хотя бы в два раза быстрее.

 

3 часа назад, MrAbad сказал:

в будущем постараюсь что-то по-глубже делать

Предлагаю начать как раз с этой темы: рассказать, что такое оптимизация, зачем нужна, и как её применять, когда улучшение одного параметра ухудшает другой. И поправить противоречивые моменты, вводящие в заблуждение. Тогда этот урок будет давать какие-то ответы, а не порождать вопросы на ровном месте.

 

В 01.04.2021 в 06:24, MrAbad сказал:

Пустая функция (function() end) занимает 93 байт, пустая функция с return (function() return true end) занимает 102 байт.

Что значит "занимает"? В какой момент? В момент создания функции или на время вызова? Нужно ли вызывать сборщик мусора после вызова функции? Или сборщик мусора тут не поможет, и требуется очищать ссылку на функцию? Значит ли это, что избавление кода от функций всегда полезно для его производительности? Чему учит эта часть урока?

 

В 01.04.2021 в 06:24, MrAbad сказал:

И, кстати, таблицы не сжимаются, то есть если туда запихать 1000 элементов, а потом удалить их, то весить она будет, как будто в ней все еще 1000 элементов

Значит ли это, что очистка элементов таблиц не имеет смысла?

 

В 01.04.2021 в 06:24, MrAbad сказал:

Автоматический сборщик мусора в опенкомпах вроде бы отключен, и вызывается только через os.sleep(0), и то не гарантированно, так что разработчик мода советует его вызывать 10 раз подряд, чтоб уж точно
И еще, автоматический сборщик чистит ТОЛЬКО локальные переменные

А если я обнулил глобальную переменную, содержащую длинную строку, разве занимаемая ей память не будет очищена сборщиком мусора? И что значит, 10 раз подряд? Что мне мешает перемежать вызовы sleep какими-то вычислениями? И что мне мешает заменить вызовы sleep, вызовами методов периферии. И обязательно ли мне каждый раз делать ровно 10 вызовов, или есть способ определить факт вызова сборщика мусора? И как определить, что вообще пора его вызывать?

 

В 01.04.2021 в 06:24, MrAbad сказал:

Не используйте рекурсию функций, а если совсем приспичило, то почаще вставляйте сбор мусора

Насколько чаще? Будет ли правильным вставлять сбор мусора в каждый вызов рекурсивной функции?

 

В 01.04.2021 в 06:24, MrAbad сказал:

Не используйте замыкания (это когда функция возвращает другую функцию)

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

 

В 01.04.2021 в 06:24, MrAbad сказал:

(Если вы не поняли, что сейчас вообще было, то ищите битовую операцию XOR)
Этот совет дает хотя бы небольшой прирост, из-за того, что нет лишний переменной, но этот плюс нивелируется, если ее удалить

Здесь я не понял, что сейчас вообще было. Откуда надо удалить переменную , чтобы этот плюс нивелировался? И что за прирост даёт этот совет?

Поделиться сообщением


Ссылка на сообщение
Поделиться на других сайтах

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить в тему...

×   Вы вставили отформатированное содержимое.   Удалить форматирование

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отобразить как ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.


×
×
  • Создать...