LINUX.ORG.RU

Предсказуемость математики и луа

 ,


1

1
function hashStr (nome)
	hours, minutes = GetGameTime()
	count1=hours* 3,1415926535
	count2=minutes* 3,1415926535
	count3=count1*count2
	count3=string.sub(count3, 1, 3)
	count3=string.format("%03d",count3)
	hNik=string.byte(nome,1)
	hNik2=string.byte(nome,2)
	hNome=hNik*hNik2
	hNome=string.sub(hNome, 1, 3)
	hNome=string.format("%03d",hNome)
	r1=string.sub(count3, 1, 1)
	r2=string.sub(hNome, 1, 1)
	r3=string.sub(count3, 2, 2)
	r4=string.sub(hNome, 2, 2)
	r5=string.sub(count3, 3, 3)
	r6=string.sub(hNome, 3, 3)
	r=r1 .. r2 .. r3 .. r4 .. r5 .. r6
	return r
end

hours, minutes = GetGameTime() получает текущие час и минуту в формате: 01 22

Скармливаем слово на одном компе - получаем предсказуемо одинаковый результат. Скармливаем на другом компе получаем тоже предсказуемо одинаковый результат, но не такой, как на предыдущем компе. Это как вообще? Данные одинаковые. Ник один и тот же. Время одно и то же. Результат всегда разный. Это вообще законно?! Время возвращается серверное - одинаковое и там и там.

Перемещено Dimez из general

★★★★★

Последнее исправление: hobbit (всего исправлений: 2)
Ответ на: комментарий от LINUX-ORG-RU

Хм.. Непонятно совсем.

function testMem()
	SAtest = {}
	for i=1,1000 do 
		SAtest[i] = {}
		for j=1, 100 do
			x=math.random(1,2)
			if x == 1 then
				SAtest[i][tostring(j)] = 1
			else
				SAtest[i][tostring(j)] = 5
			end
		end
	end
end

Вот я записал сто тысяч строк с цифрами. Это занимает в озу 5мб.

function testMem()
	SAtest = {}
	for i=1,1000 do 
		SAtest[i] = {}
		for j=1, 100 do
			x=math.random(1,2)
			if x == 1 then
				SAtest[i][tostring(j)] = true
			else
				SAtest[i][tostring(j)] = false
			end
		end
	end
end

Делаю так - 5мб.

function testMem()
	SAtest = {}
	for i=1,1000 do 
		SAtest[i] = {}
		for j=1, 100 do
			x=math.random(1,2)
			if x == 1 then
				SAtest[i][tostring(j)] = "Многомного текст, очень много"
			else
				SAtest[i][tostring(j)] = "Еще больше различного текста"
			end
		end
	end
end

Делаю так - все те же 5мб. До килобайта совпадение. Как так?

Но рядом сохраняю логи чата и они жрут до мегабайта в сутки. Я чего то не понимаю в работе памяти.

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от ann_eesti

Это общее потребление всех аддонов. Я его только что тоже проверял - все совпадает. Если создать таблицу с записями разница в общем потреблении на 5мб.

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

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от ann_eesti
function testMem()
	SAtest = {}
	for i=1,1000 do 
		SAtest[i] = {}
		for j=1, 100 do
			x=math.random(1,2)
			if x == 1 then
				SAtest[i][j] = "Многомного текст, очень много"
			else
				SAtest[i][j] = "Еще больше различного текста"
			end
		end
	end
end

Сделал вот так. Потребление стало 1,7мб.

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

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от LightDiver

Это

SAtest[i][tostring(j)] = 1

И это

SAtest[i][tostring(j)] = true

Равнофигственно, булево значение займёт места в памяти +/- столько же сколько числовое, особенно с учётом того что ты ключи делаешь строками, а это значит гарантированно распихиваешь это в хештаблицу минуя любые оптимизации в отношении непрерывных данных.

SAtest[i][tostring(j)] = "Многомного текст, очень много"

Ситуаций может быть три в некотором приближении

  • Луа переиспользует одинаковые строки, а это значит что
    • Вместо 100000 строк будет 1 55 байтовая строка и 99999 указателей на неё
  • Луа не переиспользует одинаковые строки или именно эту
    • Или переиспользует, но не всегда и не везде.

Ты сделал 1 строку "Многомного текст, очень много" и 99999 указателей на неё. В Lua одинаковые строки переиспользуются, так экономится очень много памяти, но это не закон, мол всегда так, зависит от ситуации/реализации.

Вот потребление памяти в байтах на всех версиях Lua твоих тестов, по по порядку 1==а,2==b,3==c.

dron@gnu:~/Рабочий-стол/tests$ alua a.lua 
☺─────────── luajit ────────────☺
3193895
☺─────────── lua5.1 ────────────☺
5233543
☺─────────── lua5.2 ────────────☺
5221556
☺─────────── lua5.3 ────────────☺
4197922.0
☺─────────── lua5.4 ────────────☺
3171417.0
dron@gnu:~/Рабочий-стол/tests$ alua b.lua 
☺─────────── luajit ────────────☺
3193839
☺─────────── lua5.1 ────────────☺
5233559
☺─────────── lua5.2 ────────────☺
5221108
☺─────────── lua5.3 ────────────☺
4197938.0
☺─────────── lua5.4 ────────────☺
3171417.0
dron@gnu:~/Рабочий-стол/tests$ alua c.lua 
☺─────────── luajit ────────────☺
3194095
☺─────────── lua5.1 ────────────☺
5233716
☺─────────── lua5.2 ────────────☺
5221265
☺─────────── lua5.3 ────────────☺
4198095.0
☺─────────── lua5.4 ────────────☺
3171574.0
dron@gnu:~/Рабочий-стол/tests$

Проверяется память через print(collectgarbage("count")*1024), множим на 1024 для показа байт, а не килобайт.

Но рядом сохраняю логи чата и они жрут до мегабайта в сутки.

Тут ты делаешь пару строк и кучу их копий, в чате у тебя наверняка много уникальных строк.

А так, когда одинаково и/или непрерывно то есть некоторые оптимизации, во всех остальных случаях как бог руку положит учитывая всю динамику жизненного цикла lua программы. Если ты думаешь что некими хитрыми способами сможешь сэкономить мегабайты памяти, то я тебя расстрою, нет не сможешь там уже и так всё упаковано, так как о любой вещи о котрой ты помумаешь в рамках памяти Lua уже подумали, а любую оптимизацию которая была бы универсальна и своим добавлением не давала побочек уже сделали. Правило простое, однородные по типу данные имеющие непрерывную последовательность могут иметь оптимизацию в памяти, одинаковые данные типа string будут переиспользоваться без дублирования памяти, во всех остальных случаях все данные хранятся в хештаблице с её плюсами и минусами. Всё. Про userdata и подобное я молчу, отдельная тема.

Но, во первых, в любом случае относись к словам не как к истине, а так, со скепсисом =))) И ещё в частных случаях может случится так что у тебя потребление памяти упало, бывает это после прохода GC в случае когда ты замеряешь сначала до него, а потом после и бывает в случае когда у тебя данные меняются, меньше дырок, больше непрерывных таблиц, много одинаковых строк, больше числовых ключей, в замен дыркам с нилами, разрозненной «индексации» и смешанными типами в таблицах.

А мегабайт в день из за чата это 365 мегабайт всего чата за весь год, это не много. Можешь например хранить в памяти не более 10000 сообщений, а всё остальное сбрасывать на диск, организовав циклический буффер на стыке которого начало-конец сообщения сбрасываются на диск просто записывая новое сообщение в голову, а самое старое в файл и всё и не будет у тебя никаких проблем с памятью +/- фиксированное значение с учётом того что есть лимит на размер сообщения.

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)
Ответ на: комментарий от LINUX-ORG-RU
function testMem()
	local shablon="абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	SAtest = {}
	for i = 1,500 do 
		SAtest[i] = {}
		for j = 1, 50 do
			SAtest[i][j] = ""
			for q = 1, 5 do
				print(string.utf8sub(shablon,math.random(1,117),math.random(1,117)))
				SAtest[i][j] = SAtest[i][j] .. string.utf8sub(shablon,math.random(1,118),math.random(1,118))
			end
		end
	end
end

4790.455078125 кб

function testMem()
	SAtest = {}
	for i = 1,500 do 
		SAtest[i] = {}
		for j = 1, 50 do
			SAtest[i][j] = 1
		end
	end
end

451.4638671875 хммм.. Разница в 10 раз. Интересно.

Получается, содержимое всетаки учитывается, но жестко оптимизируется. Значит всетаки есть смысл сжимать содержимое. Ок. Но нужно следить за структурами и не использовать лишнего.

Но заметь, разница ВСЕГО в 10 раз. Хотя в одном случае строки вот такого формата: ъыьДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯabcdefghijklmnдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSФХЦЧШЩЪЫЬЭЮЯabcdefghijklm"

А в другом просто 26 тысяч единиц. Не замечаешь беды?

И мне непонятно сколько занимает бул озу. Пишут, что 16 байт, а цифра по идее 8 байт. Или одинаково?

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 5)
Ответ на: комментарий от LightDiver

Посмотри как увеличивается память на каждой итерации в байтах, не на каждой итерации идёт увеличение памяти, не на размер данных идёт увеличение памяти, а на чанки например снаяала выделяется 64 затем несколько итераций тишина, затем 128 и опять тишина затем 512 и опять тишина. Это не общий объём памяти, а объём в байтах которые добавились в памяти в каждой из итераций, выделяется кусочек размером в X затем он заполняется и несоклько итераций подряд (для строк особено) память не увеличивавется.

string.utf8sub = utf8.sub

local prev = 0
function testMem()
	local shablon="абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	SAtest = {}
	for i = 1,500 do
		SAtest[i] = {}
		for j = 1, 50 do
			SAtest[i][j] = ""
			for q = 1, 5 do
				--print(string.utf8sub(shablon,math.random(1,117),math.random(1,117)))
				SAtest[i][j] = SAtest[i][j] .. string.utf8sub(shablon,math.random(1,118),math.random(1,118))
                collectgarbage("collect")
                local curr = collectgarbage("count") * 1024
                print(curr - prev)
                prev = curr
			end
		end
	end
end

testMem()
collectgarbage("collect")
print(collectgarbage("count"))
local prev = 0
function testMem()
	SAtest = {}
	for i = 1,500 do
		SAtest[i] = {}
		for j = 1, 50 do
			SAtest[i][j] = 1
            collectgarbage("collect")
            local curr = collectgarbage("count") * 1024
            print(curr - prev)
            prev = curr
		end
	end
end


testMem()
collectgarbage("collect")
print(collectgarbage("count"))

Например вот 32 цикла на lua5.4 видно что память выделяется блоками которые каждый раз выделяются всё большие и большие, но не больше 512 за раз и есть много 0.0 это те случаи когда при добавлении строки по ключу, память не тратится, а переиспользуется уже имеющаяся и/или заполняется ранее выделенная.

16.0
32.0
0.0
64.0
0.0
0.0
0.0
128.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
256.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
512.0

С числами же чуть иная ситуация

0
0
56
71
0
324
103
39
107
-34
0
27
0
0
114
0
0
0
80
0
0
98
0
0
0
0
53
0
107

Всё ещё завистит от реализации, например luajit жрёт на 1,5~2 раза больше памяти чем ванильные lua и увидеть это можно только в top/htop, так как он ещё хранить свои jit трассы в памяти, это накладные расходы. И поведение отличается при выделении памяти под строки luajit например кажется никогда не выделяет чанки более 256 байт за раз, в ванили же это 512 потолок и стоит учесть что может не вся выделенная память будет заполнена, строка может быть меньше выделенной ей памяти, но вся память будет зарезервированна этой строкой, это всё уже такие моменты которые не особо-то относятся к программированию на lua это моменты реализации самой lua, даже зная паттерн поведения при увеличении памяти, ничего особо то не сделаешь, ну знаешь ты это, ну и всё, это ничего не даст. Ну, это даст понимание как не мешать lua работать эффективнее, вот и всё.

Опять же, это динамический язык, считай это lisp порой считать что-то такое себе, ну например

local foo = {}

foo[function(x) return x * 2  end] = 1
foo[function(x) return x * 4  end] = 2
foo[function(x) return x * 6  end] = 3
foo[function(x) return x * 8  end] = 4
foo[function(x) return x * 10 end] = 5
foo[function(x) return x * 12 end] = 6
foo[function(x) return x * 14 end] = 7
foo[function(x) return x * 16 end] = 8
foo[function(x) return x * 18 end] = 9

for name,val in pairs(foo) do
    foo[name] = name(val)
    print(foo[name])
end

for name,val in pairs(foo) do
    foo[name] = name(val)
    print(foo[name])
end

Бошку сломаешь, но зачем я это привёл? А затем что модель памяти (если так можно сказать) в Lua рассчитана и на такое и она устроена так чтобы любая шизофрения подобно этой работала хорошо, нет заточки особой под что-то конкретно, должно хорошо работать всё, иначе бы было что на одних типах данных всё летает, а на других тормозит и жрёт память. Есть исключение, это как уже сказал непрерывные данные с неразряженной индексацией x = {1,5,6,7,8,9,5,4,8,115,85,166...} и всё. Ну и копии строк для переиспользования.

Исключение это luajit там очень много подводных камней и знать особенности его работы порой полезно, опять же, не для того чтобы ускорить код, а для того чтобы его не тормозить. А память оно будет жрать, можно сэкономить вызвав jit.off() например. Будет меньше жрать памяти, но медленнее работать.

Не про память, но в догонку

Есть всякие статьи типа lua memory tips and tricks или что-то подобное, но в приципе, и это просто замечательно, там всё довольно скромно и понятно, вариантов мало и в 99% они не про то как «делай вот так чтобы жрало меньше!!!», а про «не делай вот так если не хочешь чтобы не жрало», это лучше потому что по умолчанию всё и так зашибись, но если хочешь побыстрее/поэкономнее не делай вот так, чуть ограничив себя, хотя это с любыми языками сценаприев так, любая фича имеет цену, чем проще фича тем она быстрее, чем проще работа с памятью и чем более однотипна и линейна память тем она жрётся меньше, это просто физическое следствие и оно закон для любого языка, даже если ты сейчас сядешь писать новый язык, ты столкнёшься с неизбежностью, вызовы без доп обработки == быстрее, линейная память с предсказуемым доступом == экономнее, оптимизации по скорости требуют памяти, оптимизации по памяти часто деоптимизируют скорость.

Фух, всё. Я рад что ты пытливо и с интересом залезаешь в кишки, это здорово. Но в целом, чем проще твой код, тем он меньше жрёт и быстрее работает, от и всё. Но тем не менее, тебе наверное уже пора прям исходники твоего интерпретатора изучать =) Я вот не изучал нихера, так пару десятков раз в разные места залезал число глянуть и всё и всё что я узнал, ну вот это работает вот так потому что учитывая все возможные случаи наверное лучшего решения чем реализовано тут уже в природе не существует, и всё =)))

Конечно там есть места которые на вид можно улучшить, но… может это и не так, не всегда в голове держишь все случаи, одно улучшение не должно своими побочными эффектами и накладными расходами ухудшать другое.

Вот теперь точно всё. Ковыряй дальше и вникай глубже, это интересно, делай тесты и замеры и на основе их результатов действуй, так надёжнее ибо динамика зависит от данных, а данные от случая в сумме это будет просто набор правил что вот в такой ситуации с таким набором данных эффективнее вот так, или вот эдак, от и всё =)))) Наверное в целом ты про Lua знаешь уже больше чем я =) Чего я тут расписываю не знаю, но просто мне показалось что ты пытаешься найти грааль чуточку волшебный, который сделает хорошо, ну так-то его нету, но он и не нужен и так всё зашибись, а если не зашибись, значит пришло время писать модуль на Си, но не всегда это возможно значит выкручиваемся по ходу дела и обстановки своими силами как можем, так и живём. The End пуду-пиду-пууууу

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Просто столкнулся с сильным ростом потребления озу и начал копать.

function addDb(db,key,msg)
	if nsDb == nil then
		nsDb = {}
	end
	if nsDb[db] == nil then
		nsDb[db] = {}
	end	
	if nsDb[db][key] == nil then
		nsDb[db][key] = {}
	end
  	local test
  	local razmerDb = tablelength(nsDb[db][key])
  	if razmerDb > 0 then
	  	for k,v in pairs(nsDb[db][key]) do
	  		if v == msg then
	  			test = 1
	  		end
	  	end
	end
  	if test == nil then
  		local num = tostring(razmerDb+1)
  		nsDb[db][key][msg] = 1
  	end
end

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

function addDb(db,key,msg)
  nsDb = nsDb or {}
  nsDb[db] = nsDb[db] or {}
  nsDb[db][key] = nsDb[db][key] or {}
  nsDb[db][key][msg] = 1
end

По сути - то же самое однйо строкой. Без проверок и прочего.

И тут у меня возник вопрос, а что лучше: nsDb[db][key][msg] = 1 или nsDb[db][key][msg] = true

Вот и начал дальше копать.

А теперь у меня вообще общий класс для работы с данными.

https://pastebin.com/Rq7ins8b

Наверное что то такое будет.

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 1)
Ответ на: комментарий от LightDiver

Ну ты можешь посмотреть на динамику роста памяти (с учётом автоматического вызова GC)

prev = collectgarbage("count") * 1024
function meminfo(msg)
    curr = collectgarbage("count") * 1024
    print(msg ,curr - prev,curr)
    prev = curr
end

function addDb(db,key,msg,sign)
  nsDb = nsDb or {}
  nsDb[db] = nsDb[db] or {}
  nsDb[db][key] = nsDb[db][key] or {}
  nsDb[db][key][msg] = sign
  meminfo(type(sign))
end

print("type","add mem","full mem")

addDb(1,1,"hello",1)
addDb(1,2,"hello",1)
addDb(1,3,"hello",1)
addDb(1,4,"hello",1)
addDb(1,5,"hello",1)
addDb(1,6,"hello",1)
print("-----------")
addDb(1,7,"hello",true)
addDb(1,8,"hello",true)
addDb(1,9,"hello",true)
addDb(1,10,"hello",true)
addDb(1,11,"hello",true)
addDb(1,12,"hello",true)

Динамика увеличения памяти между boolean и number +/- одна. Там где ты видишь отрицательные значения это либо GC пришёл и почистил, либо таблица перестроилась в более оптимальную и пришёл GC и почистил.

dron@gnu:~/Рабочий-стол$ alua a.lua 
☺─────────── luajit ────────────☺
type	add mem	full mem
number	824	42701
number	112	42813
number	128	42941
number	112	43053
number	144	43197
number	112	43309
-----------
boolean	112	43421
boolean	112	43533
boolean	176	43709
boolean	112	43821
boolean	112	43933
boolean	112	44045
☺─────────── lua5.1 ────────────☺
type	add mem	full mem
number	375	32677
number	994	33671
number	194	33865
number	162	34027
number	226	34253
number	162	34415
-----------
boolean	166	34581
boolean	162	34743
boolean	262	35005
boolean	162	35167
boolean	134	35301
boolean	162	35463
☺─────────── lua5.2 ────────────☺
type	add mem	full mem
number	1119	28860
number	251	29111
number	186	29297
number	154	29451
number	218	29669
number	154	29823
-----------
boolean	158	29981
boolean	154	30135
boolean	254	30389
boolean	154	30543
boolean	126	30669
boolean	154	30823
☺─────────── lua5.3 ────────────☺
type	add mem	full mem
number	1095.0	28839.0
number	239.0	29078.0
number	182.0	29260.0
number	150.0	29410.0
number	214.0	29624.0
number	150.0	29774.0
-----------
boolean	152.0	29926.0
boolean	150.0	30076.0
boolean	248.0	30324.0
boolean	150.0	30474.0
boolean	2168.0	32642.0
boolean	151.0	32793.0
☺─────────── lua5.4 ────────────☺
type	add mem	full mem
number	463.0	24333.0
number	158.0	24491.0
number	174.0	24665.0
number	142.0	24807.0
number	206.0	25013.0
number	-930.0	24083.0
-----------
boolean	175.0	24258.0
boolean	142.0	24400.0
boolean	270.0	24670.0
boolean	142.0	24812.0
boolean	112.0	24924.0
boolean	142.0	25066.0
dron@gnu:~/Рабочий-стол$ 

А дело тут не в булах и числах, а в том что у тебя сложные таблицы, сообщения хранятся как ключи. память увеличивается не от булов/чисел, а от таблиц в таблицах, на них же тоже память нужна, например, будем вообще ничего не писать, вставляя nil

addDb(1,1,"hello",nil)
addDb(1,2,"hello",nil)
addDb(1,3,"hello",nil)
addDb(1,4,"hello",nil)
addDb(1,5,"hello",nil)
addDb(1,6,"hello",nil)
print("-----------")
addDb(1,7,"hello",true)
addDb(1,8,"hello",true)
addDb(1,9,"hello",true)
addDb(1,10,"hello",true)
addDb(1,11,"hello",true)
addDb(1,12,"hello",true)
☺─────────── lua5.4 ────────────☺
type	add mem	full mem
nil	408.0	24278.0
nil	134.0	24412.0
nil	150.0	24562.0
nil	118.0	24680.0
nil	182.0	24862.0
nil	118.0	24980.0
-----------
boolean	-1022.0	23958.0
boolean	144.0	24102.0
boolean	270.0	24372.0
boolean	142.0	24514.0
boolean	142.0	24656.0
boolean	112.0	24768.0

А один фиг память увеличивается каждый раз при добавлении в базу на примерно 150~200 байт, ушло просто на накладные расходы от nsDb[db][key] = nsDb[db][key] or {} и подобного, на фоне этого булы и числа как значения просто теряются, сотня байт уходит просто на создание таблицы и создания её ссылки в другой таблице ещё до момента внесения туда полезных данных в виде сообщения как ключа уже другой таблицы которая будет хранить 1 или true =)

А если ещё при каждом вызове функции делать collectgarbage() то результаты ещё поменяются чуток, особенно в начале, а потом уже нет.

Просто есть ещё момент, например некая таблица в начале может строится оптимиально, но потом данные ей не позволят оной быть она перестроится, или наоборот неоптимальная таблица будет перестроена, но вот к последним словам относить ОЧЕНЬ осторожно, я сам не смотрел пока исходники и лишь про это читал на lua-users так что могу врать. Но это видно по поведению GC, если однородные данные начать заполнять разнообразной хренью, ну не может оно больше быть оптитмизированно, значит таблица будет перестроена, создана новая обычная и придёт GC хлопнет прошлую, и ведь приходит же =) Так что я в это верю, но так-то надо смотреть исходники, опять же, все исходники всех версий, и сопоставить некое общее правило, а не какой то частный случай. Я пока к такому не готов да и не хочу конкретно это делать, так как к языку это мало отношения имеет, это вопрос чисто реализации, я например молчу про то что есть lua для браузера, а там вообще под капотом js хоть и написанный например в стиле прямого переписывания С кода на JS как в fingari, но один фиг со своими приколами.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

https://ctxt.io/2/AAB4QWUpFg

Ууууфф.. Я работал над этим где то неделю, но кажись это шедевр.

Два класса. Один содержит в себе или/и создает ключ в базе данных. Второй на основе этого класса работает с таблицей:

  1. Можно добавлять уникальные данные в таблицу, которые не могут дублироваться
  2. Можно создавать не уникальные дублирующиейся записи
  3. Можно модифицировать данные точечно
  4. Можно удалять записи
  5. Можно перемещать записи по таблицам с переиндексированием всей таблицы
ns_data_base = {} -- Создали общую базу данных
wiki = create_table:new(ns_data_base, "wiki") -- Создали экземпляр "Вики"
wiki_obj = NsDb:new(wiki:get_table(), "ГС",1) -- Создали в таблице подтаблицу с термином "ГС, в котором все данные уникальны
wiki_obj:add("Гс это гс") -- Сделали запись в термин

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

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

Затем чтение из базы тоже через этот метод… И можн начинать уже реально работать будет.

Для уникальных записей в таблицах сделал свою систему индексации. Словарь то индексациин не имеет) А у меня - имеет!

https://docs.google.com/document/d/1bJg5cMS6sHgJdtB0AOO0sLOncPz08EjgOiYks6S5zeg/edit?usp=sharing

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 2)
Ответ на: комментарий от LINUX-ORG-RU

Не могу найти инфу, какой максимальный размер таблицы в луа5.1?

Сколько элементов максимум я могу впихнуть в таблицу?

Я вот вчера экспериментировал, при создании определенного количество вроде игра выдавала ошибки биг даты.. Но хотелось бы точно понять вопрос.

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

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от LINUX-ORG-RU

Хм.. Экспериментально я столкнулся с двумя ограничениями.

  1. невозможно создать больше 524288 элементов таблицы

  2. WTF\Account\VLADGOBELEN\SavedVariables\NSQC.lua:307044: main function has more than 262143 items in a constructor

выдает такую ошибку…но все работает

Второе, похоже, какой то 18 лет назад пофикшенный баг луа. Видимо в нашей версии он остался.

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 1)
Ответ на: комментарий от LINUX-ORG-RU

А теперь совсем непонятно…

function test1(num, num2)
    testQ['test'] =  {}

    if not num2 then
        for i = 1, num do
            local index = math.floor(i / 500000) + 1  -- Вычисляем индекс на основе i
            testQ['test'][index] = testQ['test'][index] or {}  -- Инициализируем подтаблицу, если она не существует
            testQ['test'][index][i] = "1"  -- Заполняем подтаблицу значением "1"
        end
    else
        local index = math.floor(num / 500000) + 1  -- Вычисляем индекс на основе num
        testQ['test'][index] = testQ['test'][index] or {}  -- Инициализируем подтаблицу, если она не существует
        table.insert(testQ['test'][index], "1")  -- Вставляем "1" в подтаблицу
    end
    print(tablelength(testQ['test']))
    for i = 1, tablelength(testQ['test']) do
        print(tablelength(testQ['test'][i]))
    end
end

Сделал функцию теста типа. test1(762143)

Теперь лимит почему то такой:

499999 в первой таблице и 262144 во второй. Вот где логика?

хмм.. А при ограничении на табица 200к, регает сколько угодно:

[27:36]60
[27:36]199999
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:36]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:37]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]200000
[27:38]162145
LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 1)
Ответ на: комментарий от LightDiver

Сейчас нет времени расписывать, но

Ограничение на память в ванили нет 64 битная Lua сожрёт всю память на 64 битной машине и 32 битная все 4 гига на 32 битной (если без хаков). Особняком стоит luajit из за особенностей хранения указателей в версии 2.0 не более 2 с копейками гигабайт, в 2.1 побольше, но это luajit он по идее не совсем Lua, короче с ним всегда и везде костыли. Есть ограничение на количество локальных переменных, смотри конфиг сборки для каждой версии. На количество элементов в таблицах лимита нет, есть на память в целом, чаще всего лимит ограничен лишь битностью ПК, с учётом вышесказанного в виде дополнительных ограничений.

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

Если ты заметил что в Lua который встроен в программу есть какие либо лимиты, или что-то не работает так как ты хочешь и так как описано в

То смирись с этим, сделай себе пометку что вот в таких случаях, вот так вот и всё.

Теперь касаемо кода, ты хоть бы дебагал принтами =)))

Начали
 ...
+ 499996	1
+ 499997	1
+ 499998	1
+ 499999	1 -- сработала это -> local index = math.floor(i / 500000) + 1
- 500000	2 -- та-дааааа, прыг на другой ибо `i > 500000` +1 == index == 2
- 500001	2 -- и теперь мы начали заполнять другое уже
- 500002	2
- 500003	2
- 500004	2
...
- 762143	2 -- и дошли до заветного размера test1(762143)
                -- только вот он через твой прикол с `index` размазался по двум таблицам
                -- а 200000 < 500000 и поэтому все значения в 1 таблице, а не двух.  
Кончили

Я вообще не понял зачем вычислять index, если у тебя есть террабайт памяти, то на

local memory = { }
for i=1,1000000 do
    memory[i] = {}
    for j=1,1000000 do
        memory[i][j] = 1
    end
end

Неспеша сожрёт всё, вгонит в своп и поставит раком :)

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

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

У меня не луа, а wow lua, здесь что то странное.

ns_data_base = {
  {
    "1", -- [1]
    "1", -- [2]
    "1", -- [3]
  }, -- [1]
  {
    [4] = "1",
    [5] = "1",
    [6] = "1",
  }, -- [2]
  {
    [7] = "1",
    [9] = "1",
    [8] = "1",
  }, -- [3]
}

Видишь три таблица по три записи? Это одинаковые таблицы и одинаковые записи. Ну, просто ему так хочется.

Если хочу записать 500000 записей в таблицы по 100000 в таблицу, он запишет. Но при перезаходе в игру выдаст ошибку: «constant table overflow» и обнулит файл.

Если я захочу записать в одну таблицу, туда можно максимум 524288. Дальше ошибка биг дата. Вообе при разных размерах и количестве таблиц разные ошибки. Мне нужна предсказуемая расшияемая структура. Я не понимать пока логики работы.

Код я уже исправил:

function test1(num, num2, num3)
    ns_data_base =  {}

    if not num2 then
        for i = 1, num do
            local index = tonumber(math.ceil(i / num3))  -- Вычисляем индекс на основе i
            ns_data_base[index] = ns_data_base[index] or {}  -- Инициализируем подтаблицу, если она не существует
            local shablon="абвгдеёжзийк"
            local temp_b = ""
            for q = 1, 3 do
                temp_b = temp_b .. string.utf8sub(shablon,math.random(1,10),math.random(1,10))
            end
            ns_data_base[index][i] = temp_b  -- Заполняем подтаблицу значением "1"
        end
    else
        local index = math.ceil(num / num3)  -- Вычисляем индекс на основе num
        ns_data_base[index] = ns_data_base[index] or {}  -- Инициализируем подтаблицу, если она не существует
        table.insert(ns_data_base[index], "1")  -- Вставляем "1" в подтаблицу
        print('fdsfsda')
    end
    if ns_data_base[1] then
        print(tablelength(ns_data_base))
        for i = 1, tablelength(ns_data_base) do
            print(tablelength(ns_data_base[i]))
        end
    end
end
LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 1)
Ответ на: комментарий от LINUX-ORG-RU

Кажется я придумал решение. Я сделаю несколько аддонов чисто под базы данны. Без файлов, без ничего. Чисто конфиг с переменной для хранения. В переменной безопасно можно хранить до 250 тысяч записей. Вполне достаточно. Запись по проверке таблиц. Если переполнилась, использовать следущую Все.

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от LightDiver

https://www.lua.org/source/5.1/

dron@gnu:/mnt/STORAGE/ВНЕШНЕЕ/LUA-STABLE/lua-5.1.5$ grep -R "constant table overflow"
src/lparser.c:                  MAXARG_Bx, "constant table overflow");
src/lcode.c:                    MAXARG_Bx, "constant table overflow");
#####################################################################################
dron@gnu:/mnt/STORAGE/ВНЕШНЕЕ/LUA-STABLE/lua-5.1.5$ grep -R "MAXARG_Bx"
src/lopcodes.h:#define MAXARG_Bx        ((1<<SIZE_Bx)-1)
src/lopcodes.h:#define MAXARG_sBx        (MAXARG_Bx>>1)         /* `sBx' is signed */
src/lopcodes.h:#define MAXARG_Bx        MAX_INT
src/lparser.c:                  MAXARG_Bx, "constant table overflow");
src/lcode.c:                    MAXARG_Bx, "constant table overflow");
#####################################################################################
dron@gnu:/mnt/STORAGE/ВНЕШНЕЕ/LUA-STABLE/lua-5.1.5$ grep -R "MAX_INT"
src/llex.c:  if (++ls->linenumber >= MAX_INT)
src/lopcodes.h:#define MAXARG_Bx        MAX_INT
src/lopcodes.h:#define MAXARG_sBx        MAX_INT
src/llimits.h:#define MAX_INT (INT_MAX-2)  /* maximum value of an int (-2 for safety) */
src/lstring.c:  if (tb->nuse > cast(lu_int32, tb->size) && tb->size <= MAX_INT/2)
src/ltable.c:    if (j > cast(unsigned int, MAX_INT)) {  /* overflow? */
src/lparser.c:                  TString *, MAX_INT, "");
src/lparser.c:    luaY_checklimit(fs, cc->nh, MAX_INT, "items in a constructor");
src/lparser.c:  luaY_checklimit(ls->fs, cc->na, MAX_INT, "items in a constructor");
src/lcode.c:                  MAX_INT, "code size overflow");
src/lcode.c:                  MAX_INT, "code size overflow");
dron@gnu:/mnt/STORAGE/ВНЕШНЕЕ/LUA-STABLE/lua-5.1.5$

Они задали свои значения ограничений и всё. Пытаться вычислить его явное значение я не буду, лень, 524288 это в 3 байта взалит, игра наверное 32 битная ещё и прочее прочее.

Не влезает у тебя всё в память, я выше уже писал, делай кольцевой буфер и сливай старые записи на диск. Куда тебе сотни тысяч записей то держать в памяти? =)) Зачем? Опять же и особенно если эти скрипты с хреновойтучей записей зашружаются у игроков то всё правильно, тебе как мододелу дают по рукам ибо нехер =) Это везде так в модах для CS:GO/L4D2 например и прочих, тут лимит, там лимит, это нельзя, то нельзя.

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

Все модеры так живут, 100500 ограничений, а им пофигу, делают всё в обход, но так чтобы у конечных игроков ПК не отваливались :) В том и шарм и прелесть моддеров, в рамках ограниченных и ресурсов и возможностей они вынуждены выбирать не всегда прямое решение, но лучшее их доступных с учётом всех заборов вокруг причём часто нигде и никем не доккументированных.

На сем всё, я суп кушать пошёл, гы

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Вон я выше уже расписал решение. Да, игра 32битная.

Но я сделаю для начала 10 аддонов-баз-данных. В каждом переменная на 250 тысяч записей. Это для начала 2,5 миллиона строк. Пока хватит, потом можно еще создать, если что. Изи.

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от LINUX-ORG-RU

Можно же прямо в класс запилить автоматическую работу с базами.

function NsDb:new(input_table, key, is_unique)
    local new_object = setmetatable({}, self)  -- Создаем новый объект и устанавливаем метатаблицу
    -- Инициализация таблицы по ключу
    input_table[key] = input_table[key] or {}

Вот я передаю базу классу. Но что мне мешает сделать это вот так:

--блаблапроверки на размеру таблиц
_G["ns_data_base"..i][key] = _G["ns_data_base"..i][key] or {}

Получается, я просто создаю объект и передаю общее имя базы данных. Класс уже внутри себя делает проверки на размеры таблиц и записывает в нужную.

При чтении он все это учитывает и проверяет все созданные таблицы.. Должно сработать.

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 2)
Ответ на: комментарий от LightDiver

Это штоль?

О каких они константах?

Особенности байткода, 32 бита размер числа максимальный == битовое поле 6 бит на код операции и 26 бит на индекс обращения.

Там же упоминается что в версиях повыше этого ограничения нет. Но там про luajit я тебе ссылки на это не давал и в целом понятия не имею про что ты, так как ты просто привёл имя файла который кто-то написал.

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LightDiver

Это уже смотри сам, у тебя суть в том что ты разбиваешь одну например огромную таблицу с пол-ляма записей на две по четверть ляма, так ты не упираешься в лимиты, а у же как это делать, это уже вопрос десятый, как тебе удобно так и делай =)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Усе пропало, гипс снимают, клиент уезжает.

Моя вторая версия аддона пришла к своему физическому завершению. В своей структуре базы данных она избыточна.

Сегодня все рухнуло. Таблица дошла до предела и обнулилась. Вернул из бэкапа, но через двое суток снова все рухнет.

https://pastebin.com/Bs4cz9dB

Например я хранил данные так. Это ошибка.

Один элемент таблица на одну маленькую букву? На 5? На 100? нет.

Придется делать свою бинарную структуру баз данных: Хранение данных адрессно в строке. Строка в вов луа может быть до 32 тысяч символов, я возьму 25 тысяч на строку(1000 сообщений по 254 символа)…

Допустим:

  1. читаем размер первой таблицы если он меньше 200 тысяч, переносим данные последней строки в кэш-таблицу.

1.1) Если таблица 200 тысяч, читаем следущую.

  1. Смотрим размер таблицы, если он меньше адресов меньше 1000, пишем в последнюю строку и собираем строку заново

  2. Если адресов 1000, создаем новую строку, создаем в ней новый адрес заносим строку в кэш-таблицу, собираем строку.

Получится я сразу сокращаю количество объектов в таблице в 1000 раз. Я кодирую всю инфу, сокращая ее объем.

Если мне нужно таблицы под объекты, как в пасте выше, я просто указываю длину строки не 1000, а 100. В строку влезет 100 объектов по 4 байта. 4 байтов хватит для шифровки 65 миллионов объектов - хватит на все.

В итоге вместо таблица на 100 элементов, у меня строка на 100 объектов по 4 байта и в начале строки блок адресов.

Что я мог упустить? Хмм..Ну должно ж сработать.

LightDiver ★★★★★
() автор топика
Ответ на: комментарий от LightDiver

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

Ещё раз, строки в Lua не изменяемые, любое изменение строки - это создание её изменённой копии. Тоесть если ты сейчас упираешься в размеры глубины таблицы, которая работает быстро, то при работе со строками ты можешь упереться в скорость. Классика, либо быстро, но с ограничениями, либо без ограничений, но медленно =)

Но это так к слову, всё имеет свою цену. Наверное ты для упаковки будешь использовать string.pack/string.unpack дабы с точностью до байта всё записывать и читать, или конкатенировать будешь просто, не знаю, но, передпереписыванием проведи серию тестов, а то нахлобучишь пересборку строки ака блоба с данными в цикле по 100500 раз для 100500 значений, а ещё менять сам блов за одну итерацию будешь несоклько раз и так далее и обнаружишь, что всё влезает, но работает ну ооооооооооочень медленно.

Классическая медленная операция в Lua это побайтовый обход строки

for i=1,#str do 
   local value = str:sub(1,1)
   -- что-то тут делаешь
end

Такую конструкцию если запускать её более чем 1 раз для уникальной строки, использовать например довольно дорого, и обычно более выгодно её именно что 1 раз (для уникальной строки) закешировать в таблицу

for i=1,#str do 
   str_buff[#str_buff+1] = str:sub(1,1)
end

А уже когда надо обходить/менять, то обходить таблицу, а не саму строку

for i=1,#str_buff do 
   if str_buff[i] == что-то_там then
     -- что-то_тут
   end
end

Так что сначала проверь на практике задумку в тестах. И ещё момент, любое

str = "hello"
str = str1.." world!"

Приводит к тому что значение «hello» теряет ссылку на себя, а это значит за неё придёт GC, а когда приходит GC то Даниссимо, и пусть весь мир подождёт!!! :D

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Сейчас у меня так:

tbl = {
    ["объекты] = {
        ['1'] = "tree",
        ['2'] = 'house'
    {,
    ['целостность'] = {
        ['1'] = 990,
        ['2'[ = 550
    }
{

То есть у каждого пользователя несколько таблиц по 100 объектов. Получается от 6 подтаблиц, в теории больше гораздо. В каждой подтаблице стринговый ключ - сто штук. В каждом ключе по сто интов или стрингов.

Я же хочу так:

tbl = "0202 tree house 990 550"

Что мы видим? Мы читаем первый блок строки: 1006. По байту на адрес. То есть первый адрес 02. Два слова в первом объекте. Добавляем эти слова в кэш таблицу. Смотрим второй адрес - там тоже два слова. Добавляем два слова в следущую таблицу. Это временные таблицы. Они используются для каждого обращения к строкам. Из них же пересобираются строки.

Берем кэш таблицу и формируем из нее строку: в первой таблице три слова стало? Мы ее работали? Ок, тогда tbl = "03

Во второй два осталось? Ок, tbl - "0302

дальше записываем обратно: tbl = «0302 tree tree house 990 950»

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

function mysplit (inputstr, sep)
	if sep == nil then
		sep = "%s"
	end
	local t={}
	for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
		table.insert(t, str)
	end
	return t
end

Допустим, зранение чисто блоковое:

tbl = "1006 tree house 950550"

То есть перввый объект 10 символов, второй объект 6 символов. Но тогда читать стринг.сабом… Хмм

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

Или же по переменной на объект вообще, смотри:

objects = "02 house tree" -- два объекта по 5 символов
hp = "02 950550" -- два объекта по 3 символа

Вопросы лишь - чем обрабатывать лучше…

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 6)
Ответ на: комментарий от LightDiver

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

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

Тестируй крч, а по результатом замеров выбирай как действовать :)

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

Допустим не надо ничего распаковывать и упаковывать. Прямая работа со сторокой.

str = "1218 Привет   мир   Этовторая фраза"

Вот у нас в строку записано две фразы. Я изначально классу передал, что размер слова всегда 6 символов.

Теперь что мы видим? В первой фразе 12 символов. Каждое слово по 6 символов. Берем размер адреса 4+1. Теперь у нас первое слово: string.utf8sub(str,4+1,4+1+6). Получили

Вторая фраза у нас тут: 4+1+12. Первое словоее тут:

string.utf8sub(str,4+1+12,4+1+12+6)

Вроде нет вычислений. Чистая адрессная посимвольная работа.

Можно даже не буквы считать, а количество слов. Мы же знаем размер слова всегда:

str = "0203 Привет   мир   Этовторая фраза"
LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 2)
Ответ на: комментарий от LINUX-ORG-RU

Я придумал!!!!

Представь, что у нас строка 20000 символов. В ней конечно же работать не надо. Это дорого и глупо. Поэтому у нас в ей адреса подстрок. А точнее их длины:

str = '1017 привет мирэто вторая строка"

Мы хотим работать со второй строкой. Ее длина 17. Значит мы берем длину адресов+пробел+длину первой фразы - это начало второй фразы. Плюс 17 - это конец второй фразы. И вырезаем из большой фразы эту подфразу всего из 17 символов. И уже с ней работаем. Все.

LightDiver ★★★★★
() автор топика
Последнее исправление: LightDiver (всего исправлений: 1)