Как определить оптимальную глубину рассчета параметров чаров/мобов в ядре?

BladeRunner

Дрыщ, очкарик, задрот, ботан, на мамкиной шее
Хроники
  1. Interlude
  2. Chaotic Throne: High Five
  3. Goddess of Destruction Lindvior
  4. Master Class
Исходники
Присутствуют
Сборка
все
как-то поднимали мельком тему в чате, как оптимизировать рассчеты в ядре сборки. Насколько глубоко и как часто надо пересчитывать все параметры?
Например, у нас есть одетый чар, с полученными статами. как глубоко мы при каждом его действии/ударе перепроверяем/считаем его параметры? ведь по сути каждый параметр имеет глубокую ветвь рассчета от изначальных табличных параметров и исходных статов. И если заморочиться, то и табличные параметры можно еще глубже определить через функции их рассчета.
Собственно вопрос, как определить оптимальную глубину просчета параметров на результат каждого действия чара? В каких ядрах ява/птс какие решения в этом плане используются? и как по вашему эффективнее, как можно оптимизировать? Хранить максимально посчитанную таблицу параметров чара, обновляя ее при смене эквипа/бафов/обнормалов, или пересчитывать каждый тик/действие?
Понятно сформулировал вопрос?
 
Последнее редактирование:
Хранить максимально посчитанную таблицу параметров чара, обновляя ее при смене эквипа/бафов/обнормалов, или пересчитывать каждый тик/действие?
Конечно же при смене.
 
На самом деле, ввиду того, что у сущности в л2 чуть больше пары сотен параметров, которые плотно зависят друг от друга, давным давно уже изобрели систему калькуляторов.
Кратко излагаю суть:
- Статы - это по сути перечисление(enum).
- Хранилище калькуляторов представляет из себя одномерный массив размерностью совпадающий с количеством статов.
- Сам калькулятор представляет из себя просто отсортированный по order массив функций.
- При добавлении эффекта к сущности - в соответствующие калькуляторы добавляются функции саб-эффектов этого эффекта.
- При снятии эффекта с сущности - из соответствующих калькуляторов удаляются функции саб-эффектов этого эффекта.
- При необходимости посчитать какое-то конкретное значение для какого-то конкретного стата, в калькулятор передается исходное значение, которое в калькуляторе передается через все его функции по цепочке. Каждая функция применяет к значению свое действие. Калькулятор возвращает итоговое значение.

Если не страдать хуйней и не пихать в функции какой-либо IO или жесткий CPU-bound, то просчет калькулятора в среднем из 5-6 функций происходит за промежуток от 1 до 5 наносекунд. Т.е там тупо арифметика над числами с плавающей запятой, которую любой современный процессор способен производить миллиардами операций в секунду.
Это В ДОХУЯ РАЗ БЫСТРЕЕ, чем пересчитывать значения и куда-то их сохранять т.к в этом случае 100% придется все это каким-то хитрым способом синхронизировать, в то время как калькулятор нужно синхать ТОЛЬКО на добавление-удаление функций, но не на сами расчеты.
 
- При добавлении эффекта к сущности - в соответствующие калькуляторы добавляются функции саб-эффектов этого эффекта.
- При снятии эффекта с сущности - из соответствующих калькуляторов удаляются функции саб-эффектов этого эффекта.

я правильно понимаю, что из-за абьюза именно этой логики получался овербаф? и он идет через наслоение новых баффов в обход проверки на уже имеющийся такой, или через ошибку стирания старого ?
 
я правильно понимаю, что из-за абьюза именно этой логики получался овербаф? и он идет через наслоение новых баффов в обход проверки на уже имеющийся такой, или через ошибку стирания старого ?
Овербаф получается при абьюзе мозга разработчика, который не посчитал нужным сделать нормально и упорол костыль.
 
И я не совсем одуплил из твоего объявнения. при каждом обращении к параметрам чара при изменении состояния или атаке идет пересчет всех функций в калькуляторе (всех статов), или калькулятор посчитал нынешнее состояние чара, создал массив результатов, и пока нет изменения их - он берет значения из этого массива?
 
И я не совсем одуплил из твоего объявнения. при каждом обращении к параметрам чара при изменении состояния или атаке идет пересчет всех функций в калькуляторе (всех статов), или калькулятор посчитал нынешнее состояние чара, создал массив результатов, и пока нет изменения их - он берет значения из этого массива?
Никакие результаты никуда не сохраняются. Все расчеты только в реальном времени и на каждое действие. Буквально на каждое. Т.к там расчет представляет из себя цепочку простейших арифметических действий из набора функций внутри калькулятора.

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

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

спасибо, про барьерные инструкции сам погуглю )
но тогда выходит для оптимизации и снижения нагрузки стоит упрощать формулы рассчета, и сокращать их параметры
 
спасибо, про барьерные инструкции сам погуглю )
но тогда выходит для оптимизации и снижения нагрузки стоит упрощать формулы рассчета, и сокращать их параметры
Да, я про это писал выше. В функциях калькулятора не должно быть какой-то сложной логики, любого вида ожиданий и синхронизаций. Т.е например, если функция, которая считает скорость 20 раз в секунду, полезет в базу данных, чтобы проверить, есть ли у твоего клана пассивка на +1 Dex, то это ничем хорошим не кончится.
 
Да, я про это писал выше. В функциях калькулятора не должно быть какой-то сложной логики, любого вида ожиданий и синхронизаций. Т.е например, если функция, которая считает скорость 20 раз в секунду, полезет в базу данных, чтобы проверить, есть ли у твоего клана пассивка на +1 Dex, то это ничем хорошим не кончится.

а как тогда реализовываются клан-скилы? они прописаны изначально в хранилище калькуляторов как инфа о чаре?
 
а как тогда реализовываются клан-скилы? они прописаны изначально в хранилище калькуляторов как инфа о чаре?
Как правило, информация о всех клановых скиллах хранится в объекте клана, в памяти. Поэтому доступ к этим данным почти мгновенный. Я просто привел пример некорректной реализации.
 
Java:
package ru.nts.benchmarks;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class CalculatorBenchmark {

    private Calculator calculator;
    private final Random random = new Random();
    private final double initValue = random.nextDouble();

    @Param({"100000", "1000000", "10000000"})
    public int operations;

    @Setup
    public void setup() {
        Func[] funcs = new Func[10];
        for (int i = 0; i < 5; i++) {
            funcs[i] = new ADD(random.nextInt(10), random.nextDouble() * 100);
            funcs[i + 5] = new MUL(random.nextInt(10), random.nextDouble() * 10);
        }
        calculator = new Calculator(funcs);
    }

    @Benchmark
    public double operations_avg() {
        double result = 0;
        for (int i = 0; i < operations; i++) {
            result = calculator.calc(initValue);
        }
        return result;
    }

    public static void main(String[] args) throws RunnerException {
        new Runner(new OptionsBuilder().include(CalculatorBenchmark.class.getSimpleName()).build()).run();
    }

    public interface Func {    double calc(double init); int order(); }
    record ADD(int order, double value) implements Func {@Override public double calc(double init) { return init + value; }}
    record MUL(int order, double value) implements Func {@Override public double calc(double init) { return init * value; }}

    private record Calculator(Func[] funcs) {
        private Calculator(Func[] funcs) {
            this.funcs = Arrays.stream(funcs).sorted(Comparator.comparingInt(Func::order)).toArray(Func[]::new);
        }

        public double calc(double init) {
            double result = init;
            for (Func func : funcs) {
                result = func.calc(result);
            }
            return result;
        }
    }
}


Код:
Benchmark                           (operations)  Mode  Cnt    Score   Error  Units
CalculatorBenchmark.operations_avg        100000  avgt    5    1,409 ± 0,007  ms/op
CalculatorBenchmark.operations_avg       1000000  avgt    5   13,899 ± 0,025  ms/op
CalculatorBenchmark.operations_avg      10000000  avgt    5  142,827 ± 1,391  ms/op


Ну вот простой бенчмарк и простейшая реализация.
Калькулятор из 10 функций. 5 сложений и 5 умножений.
1 поток, без синхронизаций и записи чего-либо куда-либо. Сложность растет линейно вплоть до 10кк операций.
На 100.000 проходов калькулятора - 1.5 мс.
 
А по конкретнее? Что там оптимизировать? Можно все значения засунуть в какой-то cache. Но, будут проблемы с правилами пересчета либо как Aristo сказал о синхронизации при пересчете. Почему именно оптимизировать расчет параметров персонажа?

B сборках на Яве ну уж очень много оптимизировать можно. От очень длинной загрузки в десятки секунд, до операций с БД в синхронном виде которые происходят от полученных пакетов. Там толькo про оптимизации по памяти можно написать пару книг.
 
Насчет кэширования значений - не стоит еще забывать еще об одной причине того, почему стоит делать перерасчеты каждый раз когда идет обращение к стате - у стат, в конкретном значении могут быть кондишны, которые влияют на использование этого значения в данный момент. Например значение учитывается в расчете только при нахождении в определенной зоне, при уровне хп не выше определенного и т.д. и т.п.
Вот с такими кондишнами в целом и надо достаточно вменяемо работать, чтобы были достаточно простыми и отрабатывали проверки как можно быстрее и оптимальней, а то бывает в них такого нагородят, что только за голову хватаешься - в какой-то из старых сборок видел даже работу с бд в них...
 
Насчет кэширования значений - не стоит еще забывать еще об одной причине того, почему стоит делать перерасчеты каждый раз когда идет обращение к стате - у стат, в конкретном значении могут быть кондишны, которые влияют на использование этого значения в данный момент. Например значение учитывается в расчете только при нахождении в определенной зоне, при уровне хп не выше определенного и т.д. и т.п.
Вот с такими кондишнами в целом и надо достаточно вменяемо работать, чтобы были достаточно простыми и отрабатывали проверки как можно быстрее и оптимальней, а то бывает в них такого нагородят, что только за голову хватаешься - в какой-то из старых сборок видел даже работу с бд в них...
Из того что я видел обычно вешается бафф или же просто эффект на персонажа, т.е. добавляются формулы различных типов. Вот например добавление таких формул и является примером правил для пересчета. Я в принципе согласен и сделал ужe cache в своей разработке. Не нужно пересчитывать то что можно сохранить. Но главное в том что пересчитываться должно только в определенных правилах а не всегда. Ну а правил... или же ситуаций, не так уж много.
 
Кешировать имеет смысл только уже вычисленные итоговые значения, которые отправляются в клиент, для того, чтобы триггерить изменения централизовано и использовать кумулятивные обновления. Такое проще отслеживать и в этом есть определенный смысл.
 
Соглашусь что представленный вариант самый простой + эффективный.
Мой ранее предложенный вариант можно назвать инкрементальным подсчетом, потому что он условно предполагался для
Код:
val baseValue = 123.0
var finalValue = baseValue
finalValue *= 1.3
println(finalValue)
finalValue /= 1.3
println(finalValue)
но в реальном мире его достаточно сложно (невозможно) применить так, чтобы его преимущества (относительно варианта выше) принесли пользу сопоставимую с трудозатратами.
Одна из причин - динамические значения модификаторов. Тут нужно гарантировать что модификатор при удалении будет равнозначен тому, который был использован при добавлении. А как такое гарантировать без хранения списка модификаторов :)

Возник концептуальный вопрос по калькулятору с пересчетом на каждый гет, как формируются блоки с значениями статов в динамических пакетах и на каком уровне?
 
Кешировать имеет смысл только уже вычисленные итоговые значения, которые отправляются в клиент, для того, чтобы триггерить изменения централизовано и использовать кумулятивные обновления. Такое проще отслеживать и в этом есть определенный смысл.
Забыл оветить раньше. Проблема ведь в том что в клиент пересылается намного меньше значений чем есть видов калькуляторов. Да и в ядре при каждом действии персонажа будут запрашиваться различные пересчеты калькуляторов. Поэтому не имеет смысла кешировать только данные для клиента или когда данные пересылаются к клиенту. Или же можно только остановится на определенных значениях и кешировать только такие. Но смысл ведь в том чтобы просто не пересчитывать значения, вот кеширование и помогает.
Соглашусь что представленный вариант самый простой + эффективный.
Мой ранее предложенный вариант можно назвать инкрементальным подсчетом, потому что он условно предполагался для
Код:
val baseValue = 123.0
var finalValue = baseValue
finalValue *= 1.3
println(finalValue)
finalValue /= 1.3
println(finalValue)
но в реальном мире его достаточно сложно (невозможно) применить так, чтобы его преимущества (относительно варианта выше) принесли пользу сопоставимую с трудозатратами.
Одна из причин - динамические значения модификаторов. Тут нужно гарантировать что модификатор при удалении будет равнозначен тому, который был использован при добавлении. А как такое гарантировать без хранения списка модификаторов :)

Возник концептуальный вопрос по калькулятору с пересчетом на каждый гет, как формируются блоки с значениями статов в динамических пакетах и на каком уровне?
Таких пакетах как UserInfo, PetInfo и NpcInfo. Ну потом есть также StatusUpdate где пересылаются больше статов чем в других пакетах.
 
Код:
val baseValue = 123.0
var finalValue = baseValue
finalValue *= 1.3
println(finalValue)
finalValue /= 1.3
println(finalValue)
вот в такой реализации применения/убирания значений есть одна большая проблема - float/double всегда имеют определенную погрешность, начиная с определенных цифр после запятой и в итоге вроде как после умножения и обратного деления на одно и то же значение мы должны получить оригинальное значение, но в реальности оно будет уже незначительно отличаться и эта погрешность будет накапливаться при каждом применении/убирании значений.
Вроде как и мелочь, но в итоге например постоянным ребаффом одних и тех же баффов/дебаффов так разогнать или наоборот значительно уменьшить какую нибудь стату которая так вычисляется.

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

З.Ы. можно конечно для точности в подобном юзать BigDecimal вместо float/double, но это как из пушки по воробьям...
 
вот в такой реализации применения/убирания значений есть одна большая проблема - float/double всегда имеют определенную погрешность, начиная с определенных цифр после запятой и в итоге вроде как после умножения и обратного деления на одно и то же значение мы должны получить оригинальное значение, но в реальности оно будет уже незначительно отличаться и эта погрешность будет накапливаться при каждом применении/убирании значений.
Вроде как и мелочь, но в итоге например постоянным ребаффом одних и тех же баффов/дебаффов так разогнать или наоборот значительно уменьшить какую нибудь стату которая так вычисляется.

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

З.Ы. можно конечно для точности в подобном юзать BigDecimal вместо float/double, но это как из пушки по воробьям...

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