LINUX.ORG.RU

Можно здесь что-нибудь оптимизировать?

 , ,


0

2

Сделал скрипт для обработки модов для игры OpenXcom, добавляющий возможность использовать нестандартное стреляющее оружие в рукопашной (аналог Gun Melee). Он берёт YAML-файл с описанием предметов (наподобие https://pastebin.com/u39XtczR), анализирует массив «categories» и создаёт новый YAML-файл, содержащий по 4 поля для каждого стреляющего оружия: тип, силу удара, скорость и точность (наподобие https://pastebin.com/0Tkfpwpq).

use YAML qw/ LoadFile DumpFile /;
use List::Util qw/ any none /;
$YAML::UseHeader = 0;
$YAML::Preserve = 1;

my ($filename) = @ARGV;
my $data = LoadFile($filename);

my $items_new = [];

# In case the single root key isn't 'items'
my @key_list = keys(%$data);
my $single_key = $key_list[0];
for my $item (@{ $data->{ $single_key } }) {
    my $categories = $item->{categories};
    # if categories doesn't exist or is empty
    if (not $categories or not @$categories) {
        next;
    }
    # if categories doesn't contain any ranged weapon
UPD:В этом месте была моя ошибка — оказалось необходимо исключать категорию STR_CLIPS — боеприпасы.
    if ( ( none { $_ eq "STR_PISTOLS" or 
# длинный список категорий пропущен
               $_ eq "STR_RIFLES"  or 
               $_ eq "STR_LAUNCHERS" } @$categories ) or
         ( any { $_ eq "STR_CLIPS" } @$categories ) ) {
        next;
    }
    # delete all unnecessary fields; 'categories' is still needed
    foreach my $key (keys %$item) {
        if ( ($key ne 'categories') and ($key ne 'type') ) {
            delete $item->{$key};
        }
    }
    # add melee power/TUs/accuracy fields
    if (any { $_ eq "STR_PISTOLS" } @$categories) {
        $item->{meleePower}    =  20;
        $item->{tuMelee}       =  15;
        $item->{accuracyMelee} = 100;
    }
# куча присвоений пропущена 
    if (any { $_ eq "STR_LAUNCHERS" } @$categories) {
        $item->{meleePower}    =  80;
        $item->{tuMelee}       =  80;
        $item->{accuracyMelee} = 100;
    }
    delete $item->{categories};
    push @$items_new, $item;
}

$data->{ $single_key } = $items_new;

DumpFile "Gun_Melee_nonstandard.rul", $data;
Скрипт полностью: https://pastebin.com/qxzpzf7e Пример файла для обработки: https://openxcom.org/forum/index.php?action=dlattach;topic=4595.0;attach=34102

Вопрос: можно ли как-то оптимизировать этот скрипт? Сократить? Ускорить? Сейчас он в цикле удаляет из каждой ноды все поля кроме 4, а не лучше ли будет создавать новый список нод, содержащих по 4 пары имя-значение? Как это сделать? Можно ли сделать доступ к единственному элементу массива data короче, чем в 3 строки?

★★★★★

с чего ты решил, что это вообще надо оптимизировать? это какая-то мегачастая операция, двадцать миллионов запросов на парсинг в секунду?

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

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

Главная цель — не городить десяток команд там, где хватит одной-двух.

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

Долго работает. Хотелось бы быстрее

NYTProf в помощь

Вообще тут напрашивается следующая отпимизация: не парсить неиспользуемые данные и записывать выходные данные сразу в файл вместо того, чтобы потом их заново обходить. Все манипуляции со списками item'ов сразу идут нафиг, так как нет никаких списков.

Для этого нужен парсер, работающий в pull- (запрос следующего токена в цикле) или «SAX-» (т.е. коллбэки) режиме. Если можно заложиться на конкретный вид форматирования файла, то я бы вообще тупо считывал его построчно и выплевывал новый item в файл в момент получения новой строки с /^- type:/, но если входной YAML может формироваться разными способами, то придется парсить честно.

В любом случае, печатать ответ надо не через DumpFile, а print'ом на ходу в процессе анализа данных. Это быстрее и короче

*UPD* Или найти потоковый YAML-writer, будет по-красивее чем print

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

1. +1 за профилирование профайлером

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

3. Убедиться что используются XS-версии для List::Util и YAML

3. Вот тут

  # delete all unnecessary fields; 'categories' is still needed
  foreach my $key (keys %$item) {
    if ( ($key ne 'categories') and ($key ne 'type') ) {
      delete $item->{$key};

Должно быть быстрей если не удалять ненужные, а скопировать ссылки на нужные в новый хэш.

4. Вот тут

  if (none { $_ eq "STR_PISTOLS" or
               $_ eq "STR_BOWS" or
  ...
  if (any { $_ eq "STR_CANNONS" } @$categories) {
  ...    
  if (any { $_ eq "STR_MACHINE_GUNS" } @$categories) {
  ...
  if (any { $_ eq "STR_LAUNCHERS" } @$categories) {
  ...

Если сделать тоже самое за один проход, должно быть быстрей, типа

  my $skip = 1;
  for(@$categories){
    $skip = 0 if $skip and $_ eq "STR_PISTOLS" or
               $_ eq "STR_BOWS" or
               $_ eq "STR_SMGS" 
               ....
    if($_ eq "STR_MACHINE_GUNS"){
        $item->{meleePower}    =  65;
        $item->{tuMelee}       =  50;
        $item->{accuracyMelee} = 100;
    }
    if($_ eq "STR_LAUNCHERS"){
        $item->{meleePower}    =  80;
        $item->{tuMelee}       =  80;
        $item->{accuracyMelee} = 100;
    }
  }
  next if $skip;

Но судя по присланному файлу это тебе ничего не даст т.к. список categories небольшой.

Кароче профилируй.

pru-mike ★★ ()

Можно ли сделать доступ к единственному элементу массива data короче, чем в 3 строки?

Конечно.

for my $item (@{ $data->{ 'items' }[0] }) {

:-D

pru-mike ★★ ()
Ответ на: комментарий от pru-mike

Убедиться что используются XS-версии для List::Util и YAML

XS-версия меняет порядок строк в выводимом YAML. Или это можно как-то отключить?

Если сделать тоже самое за один проход, должно быть быстрей,

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

question4 ★★★★★ ()
Ответ на: комментарий от pru-mike

Конечно.

for my $item (@{ $data->{ 'items' }[0] }) {

Его имя может отличаться.

question4 ★★★★★ ()

Можно попробовать использовать более подходящий язык.

Например Rust. На нём любой быдлокод быстро работает.

«Бенч» говорит что раст в 60 раз быстрее:

./test.pl items_XCOMFILES.rul  1.80s user 0.00s system 99% cpu 1.798 total
./xcom items_XCOMFILES.rul out.rul  0.03s user 0.01s system 99% cpu 0.043 total

Код:

extern crate serde_yaml;
extern crate time;
#[macro_use] extern crate derive_error;

use std::env;
use std::fs;
use std::io::{self, Read, Write};

use serde_yaml::{Value, Sequence, Mapping};

#[derive(Debug, Error)]
enum Error {
    Io(io::Error),
    Yaml(serde_yaml::Error),
    NoItems,
}

fn main() {
    let start = time::precise_time_ns();

    // Get paths from the args.
    let args = env::args().collect::<Vec<String>>();
    if args.len() != 3 {
        println!("Usage: xcom in.rul out.rul");
        return;
    }

    if let Err(e) = process(&args[1], &args[2]) {
        println!("Error: {:?}", e);
    }

    let end = time::precise_time_ns();
    println!("Elapsed: {:.4}ms", (end - start) as f64 / 1_000_000.0);
}

fn process(in_path: &str, out_path: &str) -> Result<(), Error> {
    // Read a file into the buffer.
    let mut file = fs::File::open(in_path)?;
    let length = file.metadata()?.len() as usize;
    let mut text = String::with_capacity(length + 1);
    file.read_to_string(&mut text)?;

    // Remove BOM.
    if text.starts_with("\u{feff}") {
        text.drain(0..3);
    }

    let value: Value = serde_yaml::from_str(&text)?;

    let valid_categories = [
        "STR_PISTOLS",
        "STR_BOWS",
        "STR_SMGS",
        "STR_SHOTGUNS",
        "STR_RIFLES",
        "STR_SNIPER_RIFLES",
        "STR_CANNONS",
        "STR_MACHINE_GUNS",
        "STR_LAUNCHERS",
    ];

    // Collect items.
    let mut found_items;
    if let Some(&Value::Sequence(ref seq)) = value.get("items") {
        found_items = Vec::with_capacity(seq.len());

        for v in seq {
            if let Some(&Value::Sequence(ref categories)) = v.get("categories") {
                for category in &valid_categories {
                    if categories.iter().any(|s| s == category) {
                        if let Some(kind) = v.get("type") {
                            found_items.push((*category, kind));
                        }
                    }
                }
            }
        }
    } else {
        return Err(Error::NoItems);
    }

    // Generate items.
    let mut new_items = Sequence::with_capacity(found_items.len());
    for (category, kind) in found_items {
        let mut new_item = Mapping::new();

        new_item.insert(Value::from("type"), kind.clone());

        let (melee_power, tu_melee, accuracy_melee) = match category {
            "STR_PISTOLS" =>        (20, 15, 100),
            "STR_BOWS" =>           (20, 15, 100),
            "STR_SMGS" =>           (20, 15, 100),
            "STR_SHOTGUNS" =>       (50, 40, 100),
            "STR_RIFLES" =>         (50, 40, 100),
            "STR_SNIPER_RIFLES" =>  (50, 40, 100),
            "STR_CANNONS" =>        (65, 50, 100),
            "STR_MACHINE_GUNS" =>   (65, 50, 100),
            "STR_LAUNCHERS" =>      (80, 80, 100),
            _ => unreachable!(),
        };

        new_item.insert(Value::from("meleePower"), Value::from(melee_power));
        new_item.insert(Value::from("tuMelee"), Value::from(tu_melee));
        new_item.insert(Value::from("accuracyMelee"), Value::from(accuracy_melee));

        new_items.push(Value::Mapping(new_item));
    }

    let mut new_value = Mapping::new();
    new_value.insert(Value::from("items"), Value::Sequence(new_items));

    let text = serde_yaml::to_string(&Value::Mapping(new_value))?;
    let mut file = fs::File::create(out_path)?;
    file.write_all(text.as_bytes())?;

    Ok(())
}

[package]
name = "xcom"
version = "0.1.0"

[dependencies]
serde = "1.0"
serde_yaml = "0.7"
time = "0.1"
derive-error = "0.0.3"

[profile.release]
opt-level = 3
lto = true
RazrFalcon ★★★★★ ()
Ответ на: комментарий от pru-mike

Должно быть быстрей если не удалять ненужные, а скопировать ссылки на нужные в новый хэш.

Как это сделать? Я плохо знаю Перл, после push хеш превращался в 8 скаляров.

    my %new_item = ( type => $item->{type},
            meleePower    => $item->{meleePower},
            tuMelee       => $item->{tuMelee},
            accuracyMelee => $item->{accuracyMelee}
    );
    push @$items_new, %new_item;
Что здесь изменить?

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

Спасибо, пробую.

Никогда не пользовался Растом, поэтому вопросы:

Где взять serde_yaml и time? serde_yaml тянет ещё какие-то зависимости?

Как их правильно устанавливать под Gentoo?

Как компилировать? Достаточно написать rustc yaml5.rs или необходимо прописывать кучу путей как для GCC?

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

Это в полях хэша items или где?

В создаваемом файле. В полях каждого хеша, входящего в items. Сортирует в алфавитном порядке, например. Для YAML без XS сортировка выключается

$YAML::Preserve = 1;
но для YAML::XS такую опцию на #yaml@FreeNode не знают.

Мелочь, но неприятно.

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

В полях каждого хеша, входящего в items.

1. Я подозреваю, что сохранение порядка и опция $YAML::Preserve = 1;
это особенность реализации YAML::PP. (pure-perl)
Т.е. в других перловых реализация и на других языках и платформах
такой возможности не будет и порядок ключей в выходном файле будет другим (не тем что был на входе).
И опция Preserve должна снижать производительность, может быть даже существенно.

2. Давно не пользовался YAML-ом, но там в доке http://search.cpan.org/~ingy/YAML-1.23/lib/YAML.pod
есть SortKeys Default is 1. (true).
Может быть надо попробовать её выключить.

pru-mike ★★ ()
Ответ на: комментарий от question4

Сортирует в алфавитном порядке, например.

А чем кстати она мешает?
Сортировка в данному случае никак влиять не должна

pru-mike ★★ ()
Ответ на: комментарий от pru-mike

Привычнее определённый порядок, особенно чтобы type шёл первым.

Сейчас пробую разные комбинации. Похоже, алфавитная сортировка ускоряет на несколько процентов.

question4 ★★★★★ ()

Вместо большоого списка $_ eq «STR_PISTOLS» or - сделай большую регулярку. Скомпилированные они работают быстрее.

OxiD ★★★★ ()

Я бы засунул все строки типа «STR_PISTOLS» и соответствующие им значения полей в хэш-таблицу или sqlite. Тогда поиск и обновление по ключу делается тривиально в пару строчек. Писать это на компилируемом языке типа C/++/#/Rust - перебор. Python,Lua,Perl - в самый раз для таких задач.

anonymous ()

Первое.

#!/usr/bin/env perl

use strict;
use warnings FATAL => 'all';

use Benchmark qw(:all);
use List::Util qw(none);

my $categories = [0 .. 100];

my $regexp = qr{^(?:STR_PISTOLS|STR_RIFLES|STR_LAUNCHERS)$}x;

my $categories_hashref = {
    STR_PISTOLS   => 1,
    STR_RIFLES    => 1,
    STR_LAUNCHERS => 1,
};

cmpthese(1_000_000, {
    hash => sub {
        none { exists $categories_hashref->{$_} } @{ $categories };
    },
    regex => sub {
        none { $_ =~ $regexp } @{ $categories };
    },
    inline => sub {
        none {
            $_ eq 'STR_PISTOLS' or
            $_ eq 'STR_RIFLES'  or
            $_ eq 'STR_LAUNCHERS'
        } @{ $categories };
    },
});

Проще всего с хешами, наглядно (важно правильно назвать переменную) и быстро.

           Rate  regex inline   hash
regex   46104/s     --   -62%   -74%
inline 120048/s   160%     --   -33%
hash   179856/s   290%    50%     --

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

Третье. Представить $categories как хэш, чтобы уйти от any.

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

Можно срезы хэшей использовать для лаконичности:

 @{$item}{qw/meleePower tuMelee accuracyMelee /} = (80,80,100); 

anonymous ()

Добавь обязательно

use strict;
use warnings;

anonymous ()
Ответ на: комментарий от anonymous

На самом деле

if (not $categories or not @$categories) {
    next;
}

можно заменить на

next unless $categories && @{ $categories };

оно еще и быстрее будет.

#!/usr/bin/env perl

use strict;
use warnings FATAL => 'all';

use Benchmark qw(cmpthese);

my $categories = [];

cmpthese(1_000_000, {
    origin => sub {
        foreach (0 .. 99) {
            if (not $categories or not @$categories) {
                next;
            }
        }
    },
    postfix => sub {
        foreach (0 .. 99) {
            next unless $categories && @{ $categories };
        }
    },
});
            Rate  origin postfix
origin  121655/s      --    -35%
postfix 185874/s     53%      --
outtaspace ★★★ ()
Ответ на: комментарий от outtaspace

Слишком сложная для unless операция. Я эту конструкцию не использую иначе как для:

next unless $foo;

anonymous ()
Ответ на: комментарий от outtaspace

Разница между постфиксными if и unless всего 3 процента, а читабельность у второго сильно хуже.

anonymous ()
Ответ на: комментарий от anonymous

Для Perl 5.20.2 разница гуляет от 2 до 3 процентов. Вероятно в более новых версиях это как-то оптимизировали.

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

Ничего не получится — против кристала играет хейт по отношению к руби.

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

my $regexp = qr{^(?:STR_PISTOLS|STR_RIFLES|STR_LAUNCHERS)$}x;

my $categories_hashref = { STR_PISTOLS => 1, STR_RIFLES => 1, STR_LAUNCHERS => 1, };

Тут всплыла тонкость, которую я сразу не учёл. Помимо того, что в списке категорий должен быть какой-то из типов оружия, там не должно быть категории «STR_CLIPS». По её отсутствию отличают оружие от боеприпасов. Как быть в этом случае?

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

Что это даст? При повторном вызове того же набора categories не будет повторно вычисляться? Будет ли вычисляться повторно, если набор тот же, но в другом порядке?

Третье. Представить $categories как хэш, чтобы уйти от any.

Пройтись по всему массиву, создав новый, содержащий только type и хеш с категориями как выше? Или создавать в старом хеши на месте массивов? И как потом сравнивать? И главное: как превратить массив в хеш? Так?

my %hash = map {($_, 1)} @array;

question4 ★★★★★ ()
Ответ на: комментарий от question4
if ( ( none { $_ eq "STR_PISTOLS" or 
               $_ eq "STR_RIFLES"  or 
               $_ eq "STR_LAUNCHERS" } @$categories ) or
         ( any { $_ eq "STR_CLIPS" } @$categories ) ) {
        next;
}

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

В этом примере у нас статично условие, т.е. категории которые перечислены в none и any. Меняется только массив $categories. Если это так, то можно выделить хэш под категории из none и хэш под категории из any. Всё свести к поиску по хэшам, так мы избавимся от медленного громоздкого условия. Еще можно написать функцию, с правильным названием описывающим предметную область, возвращающая булево значение для $categories:

next unless tratata($categories)

Про мемоизацию пока забудем. Выше описано достаточно быстрое и простое решение.

my %tratata = map { $_ => 1 } @{ $categories };

if (exists $tratata{'STR_PISTOLS'}) {
   ...
}

if (exists $tratata{'STR_LAUNCHERS'}) {
    ...
}
outtaspace ★★★ ()
Последнее исправление: outtaspace (всего исправлений: 1)
Ответ на: комментарий от outtaspace

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

До прошлой недели на Перле ничего не писал, кроме простейших примеров. И те года 4 назад :)

question4 ★★★★★ ()

Пофиг на скорость, однако, интерес к оптимизации может подсказать хорошие подходы. Код написан не в perl-style, жутко бомбит от такой лапши. Нужно переписать, оптимизировать сам код, а не скорость. Присмотрись к Perl::Critic. В принципе, здесь уже дали все советы, незнаю можно ли добавить что-либо еще.

Deleted ()
Ответ на: комментарий от question4

use cargo luke. Он сам тебе создаст струкруру директорий, выставит флаги и докачает пакеты. Ну и он в стандартном пакете с растом идет.

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

use cargo luke. Он сам тебе создаст струкруру директорий, выставит флаги и докачает пакеты. Ну и он в стандартном пакете с растом идет.

Уже нашёл. В Gentoo это разные пакеты. Причём версия cargo выше: dev-util/cargo-0.21.0 и dev-lang/rust-1.19.0.

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

Пофиг на скорость, однако, интерес к оптимизации может подсказать хорошие подходы. Код написан не в perl-style, жутко бомбит от такой лапши. Нужно переписать, оптимизировать сам код, а не скорость. Присмотрись к Perl::Critic.

Ради этого и спрашивал :)

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

На сайте раста есть указание как актуальную версию всего вкатить одной командой из терминала. Причем в /home/user/.rust или как-то так. Так что будешь иметь актульные и совместимые утилиты. И удалить можно всегда все одной командой. А в репах они во-первых по отдельности, во-вторых размажутся по /usr/lib, /usr/bin и проч.

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

Не до того пока. Отпуск закончился, до пятницы вряд ли будет время.

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

cargo не зависит от компилятора и развивается параллельно.

RazrFalcon ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.