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

BladeRunner

Бывалый
Меценат
Участник Новогоднего Фонда 2024
Победитель в номинации 2024
Сообщения
1 440
Розыгрыши
0
Репутация
-269
Реакции
520
Баллы
615
Хроники
  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 в своей разработке. Не нужно пересчитывать то что можно сохранить. Но главное в том что пересчитываться должно только в определенных правилах а не всегда. Ну а правил... или же ситуаций, не так уж много.
 
Кешировать имеет смысл только уже вычисленные итоговые значения, которые отправляются в клиент, для того, чтобы триггерить изменения централизовано и использовать кумулятивные обновления. Такое проще отслеживать и в этом есть определенный смысл.
 
Назад
Сверху Снизу