Verification: a143cc29221c9be0

Php break for loop in switch

Php break for loop in switch

Немного предыстории

Откуда вообще возникла такая задача — разбирать строки как можно быстрее?

Я уже рассказывал, что у нас в «Тензоре» используется много сотен серверов PostgreSQL. И чтобы приглядывать за актуальной производительностью запросов на них, мы разработали коллектор-анализатор логов этой СУБД, который «выцепляет» из потока от сервера планы запросов, разбирает их и вычисляет нагрузку для каждого отдельного узла, что не так уж и просто.

То есть надо уметь «сидеть на потоке» и быстро-быстро анализировать (а потому — иметь максимальную производительность и минимальный прирост памяти) примерно вот такие блоки текста, а среди них — и наши buffers-строки:

Hash Left Join (actual time=9.248..51.659 rows=551 loops=1)
  Hash Cond: (c.reloftype = t.oid)
  Buffers: shared hit=5814 read=251 dirtied=63
  ->  Hash Join (actual time=2.990..7.148 rows=551 loops=1)
        Hash Cond: (c.relnamespace = nc.oid)
        Buffers: shared hit=4249 read=2
        ->  Seq Scan on pg_class c (actual time=0.046..3.922 rows=555 loops=1)
              Filter: ((relkind = ANY ('{r,v,f,p}'::"char"[])) AND (pg_has_role(relowner, 'USAGE'::text) OR has_table_privilege(oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)))
              Rows Removed by Filter: 3308
              Buffers: shared hit=1829
        ->  Hash (actual time=2.931..2.931 rows=7 loops=1)
              Buckets: 1024  Batches: 1  Memory Usage: 9kB
              Buffers: shared hit=2420 read=2
              ->  Seq Scan on pg_namespace nc (actual time=0.035..2.912 rows=7 loops=1)
                    Filter: (NOT pg_is_other_temp_schema(oid))
                    Rows Removed by Filter: 784
                    Buffers: shared hit=2420 read=2
  ->  Hash (actual time=6.199..6.199 rows=1629 loops=1)
        Buckets: 2048  Batches: 1  Memory Usage: 277kB
        Buffers: shared hit=105 read=162 dirtied=63
        ->  Hash Join (actual time=0.338..5.640 rows=1629 loops=1)
              Hash Cond: (t.typnamespace = nt.oid)
              Buffers: shared hit=105 read=162 dirtied=63
              ->  Seq Scan on pg_type t (actual time=0.015..4.910 rows=1629 loops=1)
                    Buffers: shared hit=57 read=162 dirtied=63
              ->  Hash (actual time=0.307..0.307 rows=791 loops=1)
                    Buckets: 1024  Batches: 1  Memory Usage: 86kB
                    Buffers: shared hit=48
                    ->  Seq Scan on pg_namespace nt (actual time=0.004..0.121 rows=791 loops=1)
                          Buffers: shared hit=48

Формат строки

В общем случае, формат описан в исходниках PostgreSQL. Если представить его в виде JS-кода, то получится что-то вроде:

const keys = [
  ['shared', ['hit', 'read', 'dirtied', 'written']]
, ['local',  ['hit', 'read', 'dirtied', 'written']]
, ['temp',   ['read', 'written']] // да, тут другой набор ключей 2-го уровня
];

let str = 'Buffers: ' + // константное начало
  keys
    .filter(([keyo, keysi]) => node[keyo])
    .map(([keyo, keysi]) => [
        keyo
      , ...keysi
          .filter(keyi => node[keyo][keyi] > 0)
          .map(keyi => `${keyi}=${node[keyo][keyi]}`)
      ].join(' ') // внутри собираем сегменты через пробел
    )
    .join(', ');  // снаружи - через запятая-пробел

Методика тестирования

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

Buffers: shared hit=31770
Buffers: shared hit=1159
Buffers: shared hit=255
Buffers: shared hit=2579 read=2961 dirtied=3
Buffers: shared hit=3 read=1
Buffers: shared hit=205 read=44
Buffers: shared hit=230 read=34 dirtied=3
Buffers: shared hit=13
Buffers: shared hit=5
Buffers: shared hit=6
...

Чтобы исключить возможное влияние GC, запускать наши тесты будем с ключами --expose-gc --initial-old-space-size=1024. Оцениваем всех участников по двум показателям: общее время работы и прирост объема памяти, который пришлось использовать (и на чистку которого потом придется потратить время GC и ресурсы CPU).

Шаблон нашего теста будет выглядеть примерно так:

const fs = require('fs');
const heapdump = require('heapdump');

const buffers = fs.readFileSync('buffers.txt').toString().split('\n');

const parseBuffers = line => {
// -- 8

Что ж… Приступим, и постараемся грамотно использовать все возможности, которые дают нам современные версии языка.

И начнем с самого простого.

Бронза: обыкновенный .split


const parseBuffers = line => {
  let rv = {};
  line.slice('Buffers: '.length)              // "константное" начало нас не интересует
    .split(', ').forEach(part => {            // 'shared ..., local ..., temp ...' => ['shared ...', 'local ...', 'temp ...']
      let [kind, ...pairs] = part.split(' '); // 'shared hit=1 read=2' => ['shared', ['hit=1', 'read=2']]
      pairs.forEach(pair => {
        let [type, val] = pair.split('=');    // 'hit=1' => ['hit', '1']
        rv[`${kind}-${type}`] = Number(val);  // ['shared-hit'] = 1
      });
    });
  return rv;
};

Time, avg: 544ms
Size Delta: +14.8MB:
 - (sliced string) : +6.8 // сегменты строк без 'Buffers: '
 - (string)        : +6.3 // строки имен ключей
 - (array)         : +1.7 // массивы pairs

Серебро: .lastIndex + итерация по .matchAll(RegExp)

Итак, сделаем выводы из предыдущего теста: .slice и .split нам не друзья, как и динамическая генерация имен ключей.

С именами ключей все понятно — давайте сгенерируем их все заранее, их же всего 10 вариантов. А вот .slice строки мы использовали, только лишь чтобы каждый раз «сдвинуть» начало анализа на одинаковое начало 'Buffers: '. А нельзя ли как-то сделать это без порождения новых строк?

Оказывается, можно, если использовать принудительную установку re.lastIndex для «глобального» RegExp.

Подробнее про g- и y-ключи и использование .lastIndex для более точного применения RegExp.

На этот раз будем искать в строке только те ключевые слова, которые нас интересуют:

const buffersRE = /(shared|local|temp)|(hit|read|dirtied|written)=(\d+)/g;

const buffersKeys = {
  'shared' : {
    'hit'     : 'shared-hit'
  , 'read'    : 'shared-read'
  , 'dirtied' : 'shared-dirtied'
  , 'written' : 'shared-written'
  }
, 'local' : {
    'hit'     : 'local-hit'
  , 'read'    : 'local-read'
  , 'dirtied' : 'local-dirtied'
  , 'written' : 'local-written'
  }
, 'temp' : {
    'read'    : 'temp-read'
  , 'written' : 'temp-written'
  }
};

const parseBuffers = line => {
  let rv = {};

  let keys;
  buffersRE.lastIndex = 9; // сдвигаем начало поиска на 'Buffers: '.length
  for (let match of line.matchAll(buffersRE)) {
    if (match[1]) {
      keys = buffersKeys[match[1]];
    }
    else {
      rv[keys[match[2]]] = Number(match[3]);
    }
  }
  return rv;
};

Time, avg: 270ms
Size Delta: +8.5MB

Золото: полнопозиционный .match(RegExp)

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

Чтобы не заниматься «двухуровневым» спуском по словарю за именами ключей, построим такой

страшный

RegExp, каждая захватываемая позиция которого всегда соответствует одному и тому же имени ключа. Позиции отсутствующих в строке ключей будут заполнены в match-массиве undefined:

const buffersRE = /^Buffers:(?:,? shared(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? local(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? temp(?: read=(\d+))?(?: written=(\d+))?)?$/;

const buffersKeys = ['shared-hit', 'shared-read', 'shared-dirtied', 'shared-written', 'local-hit', 'local-read', 'local-dirtied', 'local-written', 'temp-read', 'temp-written'];

const parseBuffers = line => 
  line.match(buffersRE)
    .slice(1) // в match[0] лежит исходная строка, которая нам не нужна
    .reduce(
      (rv, val, idx) => (val !== undefined && (rv[buffersKeys[idx]] = Number(val)), rv)
    , {}
    );

Time, avg: 111ms
Size Delta: +8.5MB

Наблюдательный читатель сразу же задаст вопрос — разве не будет быстрее, если убрать из регулярки константное начало '^Buffers:':

const buffersRE = /(?:,? shared(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? local(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? temp(?: read=(\d+))?(?: written=(\d+))?)?$/;

Ведь результат от этого не должен никак измениться? Но — нет, такой вариант на четверть хуже:

Time, avg: 140ms

Дело в том, что наш «полный» RegExp /^...$/ не содержит ни одной переменной части, а в случае «без начала» для каждой позиции этого сегмента приходится проверять, не начинается ли тут один из «хвостов» (shared ...|local ...|temp ...) — что требует гораздо больше ресурсов, чем просто впустую проверить совпадение двух подстрок.

Вне конкурса: скрещиваем ужа и ежа

В предыдущем варианте мы все-таки внесли начало строки в регулярку, и оно проверяется каждый раз! Давайте же воспользуемся методом с .lastIndex:

  • но он работает только с «глобальными» RegExp
  • для «глобального» RegExp обычный .match захватывает сразу всю строку, а не позиционно
  • для получения позиционного набора нам придется использовать первый (а на самом деле, единственный) результат итератора .matchAll


const buffersRE = /(?:,? shared(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? local(?: hit=(\d+))?(?: read=(\d+))?(?: dirtied=(\d+))?(?: written=(\d+))?)?(?:,? temp(?: read=(\d+))?(?: written=(\d+))?)?$/g;

const buffersKeys = ['shared-hit', 'shared-read', 'shared-dirtied', 'shared-written', 'local-hit', 'local-read', 'local-dirtied', 'local-written', 'temp-read', 'temp-written'];

const parseBuffers = line => {
  buffersRE.lastIndex = 8; // 'Buffers:'.length
  return line.matchAll(buffersRE).next().value
    .slice(1)
    .reduce(
      (rv, val, idx) => (val !== undefined && (rv[buffersKeys[idx]] = Number(val)), rv)
    , {}
    );
};

Time, avg: 304ms
Size Delta: +8.5MB

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

Итого

В сегодняшнем забеге кубок вручается обычному полнопозиционному .match(RegExp). Ура, товарищи!

JavaScript Программы

Компьютерная программа - это список "инструкций", которые должны быть "выполнены" компьютером.

На языке программирования эти инструкции по программированию называются заявлениями (или объявлениями).

Программа на JavaScript - это список программных инструкций.

В HTML программы JavaScript выполняются непосредственно веб-браузером (на стороне клиента).


JavaScript Инструкции

JavaScript инструкции состоят из: значений, операторов, выражений, ключевых слов и комментариев.

Эта инструкция указывает браузеру написать "Hello Dolly." внутри HTML элемента с id="demo":

Большинство программ JavaScript содержат много различных JavaScript заявлений (инструкций).

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

Программы JavaScript (JavaScript заявления / инструкции) часто называют JavaScript кодом.


JavaScript Точка с запятой ;

Точки с запятой разделяют JavaScript инструкции.

Добавьте точку с запятой в конце каждой выполняемой инструкции:

var a, b, c;     // Заявить 3 переменные a, b, c
a = 5;           // Присвоить значение 5 переменной а
b = 6;           // Присвоить значение 6 переменной b
c = a + b;       // Присвоить сумму a и b переменной c

Попробуйте сами »

Когда они разделены точкой с запятой, допускается несколько инструкций в одной строке:

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


JavaScript Пробел

JavaScript игнорирует несколько пробелов. Вы можете добавить лишние пробелы в ваш скрипт, чтобы сделать его более читабельным.

Следующие строки эквивалентны:

var person = "Hege";
var person="Hege";

Хорошей практикой является размещение пробелов вокруг операторов ( = + - * / ):


JavaScript Длина строки и разрывы строк

Для лучшей читаемости программисты часто предпочитают избегать строк кода длиннее 80 символов.

Если JavaScript объявление не помещается в одну строку, лучше всего его прервать после оператора (перенести на следующую строку):


JavaScript Блоки кода

JavaScript инструкции могут быть сгруппированы в блоки кода внутри фигурных скобок {...}.

Целью блоков кода является определение инструкций, которые будут выполняться вместе.

В одном месте вы найдете инструкции, сгруппированные по блокам, в функциях JavaScript:

Примеры

function myFunction() {
  document.getElementById("demo1").innerHTML = "Hello Dolly!";
  document.getElementById("demo2").innerHTML = "How are you?";
}

Попробуйте сами »

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


Цикл for

Аналогично PHP, Blade умеет работать с простейшим циклом "for", у которого задан шаг, минимальное и максимальное значение:

@for ($i = 0; $i 

Цикл forelse

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

@if ( count($ar) )
   @foreach ($ar as $el)
      
{{ $el }}
@endforeach @else
Массив пустой!
@endif

Благодаря шаблонизатору Blade можно переписать этот код более компактно с помощью "@forelse", при этом не потеряв логики (код будет делать то же самое):

@forelse ($ar as $el)
      
{{ $el }}
@empty
Массив пустой!
@endforelse

"forelse" выполняет цикл только в том случае, если обходимый массив не является пустым. Если же он пустой, то выполняется директива "@empty", которая ставится после него.

Операции внутри цикла

Как и язык программирования PHP, шаблонизатор Blade умеет распознавать различные операции внутри цикла. К примеру, аналоги этих операций: "@break" (прерывает цикл) и "@continue" (приступает к следующей итерации цикла). Продемонстрируем их работу:

@foreach ($ar as $key => $el)
   @if ($el == 5)
      
Ключ: {{ $key }}, значение: {{ $el }}
Прерываем цикл!
@break @endif @if ($el > 10)
Переходим к следующей итерации!
@continue @endif @endforeach

Заявление break

Вы уже видели break заявление, использованный в предыдущей главе этого руководства. Его использовали для "выпрыгивания" из switch() заявления.

Заявление break также можно использовать для выхода из цикла.

Заявление break разрывает петлю и продолжает выполнение кода после цикла (если таковые имеются):

Пример

for (i = 0; i   if (i === 3) { break; }
  text += "The number is " + i + "
";
}

Попробуйте сами »


Заявление continue

Заявление continue прерывает одну итерацию (в цикле), если возникает указанное условие, и продолжает работу со следующей итерацией в цикле.

В этом примере пропускается значение 3:

Пример

for (i = 0; i   if (i === 3) { continue; }
  text += "The number is " + i + "
";
}

Попробуйте сами »