Русский
Русский
English
Статистика
Реклама

С

Recovery mode Найти комбинацию соседних чисел, имеющих самое большое произведение

10.08.2020 10:05:17 | Автор: admin

Перед вами таблица (20x20) с целым числом от 0 до 99 в каждой из клеток.
Задача найти 4 соседних числа без разрыва цепочки, одно за другим, имеющих самое большое произведение. Цветом выделены различные варианты 4 соседних чисел (соседними считаются два числа, расположенных не более чем на 1 клетку друг от друга)
Сегодня мы с вами реализуем алгоритм поиска на языке C#

Для начало создадим двумерный массив 20х20 и запишем его в файл
static void CreateArrayFile(){    Random random = new Random();    string file = "";    for (int i = 0; i < 20; i++)    {        string line = "";        for (int j = 0; j < 20; j++)        {            line += random.Next(0, 99) + ",";        }        line = line + Environment.NewLine;        file += line;    }    using (FileStream fstream = new FileStream($"array.txt", FileMode.OpenOrCreate))    {        byte[] array = System.Text.Encoding.Default.GetBytes(file);        fstream.Write(array, 0, array.Length);        Console.WriteLine("Массив записан в файл");    }}


Теперь мы можем, записать числа из файла в двумерный массив.
string[] lines = arrayFile.Split(Environment.NewLine);for (int i = 0; i < 20; i++){    string[] items = lines[i].Split(',');    for (int j = 0; j < 20; j++)    {        table[i, j] = Convert.ToInt32(items[j]);    }}

Для представления координат и значения числа создадим класс Element
public class Element{    public int Line { get; set; }    public int Column { get; set; }    public int Value { get; set; }}

По условиям задачи, требуется найти комбинацию произведений. Реализуем класс Multiplication для представления комбинации содержащий массив чисел и значение произведения чисел в комбинации.
public class Multiplication{    public Multiplication()    {        Elements = new Element[4];    }    public Element[] Elements { get; set; }    public int Value    {        get        {            Multiply();            return value;        }    }    int value;    void Multiply()    {        if(Elements[0] != null && Elements[1] != null && Elements[2] != null && Elements[3] != null)        {            value = Elements[0].Value * Elements[1].Value * Elements[2].Value * Elements[3].Value;        }    }}

Первое что нужно сделать найти ближайших соседей числа. Например, для числа 40 в нашем случае следующие соседи:

А у числа 48 в правом нижнем углу есть всего 3 соседа. Не трудно понять, что соседи любого числа это
table[i-1][j-1]table[i-1][j]table[i-1][j+1]table[i][j-1]table[i][j+1]table[i+1][j-1]table[i+1][j]table[i+1][j+1]

Убедившись, что индекс не находится вне границ, получаем метод FindNeighbors возвращающий коллекцию всех ближайших соседей конкретного числа.
По условию задачи, нам нужно найти комбинацию из 4 соседних чисел. Для этого нам нужен метод для поиска всех возможных комбинаций из 4 чисел
static List<Multiplication> GetFactorCombinations(int line, int column){    List<Multiplication> combinations = new List<Multiplication>();    if (table[line, column] != 0)    {        List<Element> firstLevelNeighbors = FindNeighbors(line, column);        foreach (var firstLevelNeighbor in firstLevelNeighbors)        {            Element[] elements = new Element[4];            elements[0] = new Element            {                Line = line,                Column = column,                Value = table[line, column]            };            elements[1] = firstLevelNeighbor;            List<Element> secondLevelNeighbors = FindNeighbors(firstLevelNeighbor.Line, firstLevelNeighbor.Column);            foreach (var secondLevelNeighbor in secondLevelNeighbors)            {                if (!elements[0].Equals(secondLevelNeighbor) && !elements[1].Equals(secondLevelNeighbor))                {                    elements[2] = secondLevelNeighbor;                }                if (elements[2] != null)                {                    List<Element> thirdLevelNeighbors = FindNeighbors(secondLevelNeighbor.Line, secondLevelNeighbor.Column);                    foreach (var thirdLevelNeighbor in thirdLevelNeighbors)                    {                        if (!elements[0].Equals(thirdLevelNeighbor) && !elements[1].Equals(thirdLevelNeighbor) && !elements[2].Equals(thirdLevelNeighbor))                        {                            elements[3] = thirdLevelNeighbor;                            Multiplication multiplication = new Multiplication                             {                                 Elements = elements                            };                            if (combinations.Where(p=>p.Elements[0].Value==elements[0].Value&& p.Elements[1].Value == elements[1].Value && p.Elements[2].Value == elements[2].Value && p.Elements[3].Value == elements[3].Value).FirstOrDefault()==null)                            {                                var nnnn =new Multiplication                                {                                    Elements = new Element[] { elements[0], elements[1], elements[2], elements[3]}                                };                                combinations.Add(nnnn);                            }                        }                    }                }            }        }    }    return combinations;}

Метод получает координаты числа в таблице и проверяет значение этого числа на 0 (При умножении любого числа на 0 всегда получается 0). Потом метод ищет всех соседей данного числа. Перебираем соседей первого уровня, в случае если число не 0 ищем всех соседей второго уровня
Переопределяем метод Equals для сравнивания чисел:
public override bool Equals(Object obj){    if (this==null || (obj == null) || !this.GetType().Equals(obj.GetType()))    {        return false;    }    else if(Line == ((Element)obj).Line && Column == ((Element)obj).Column)    {        return true;    }    else    {        return false;    }}

Продолжаем поиск до соседей четвертого уровня если числа не дублируются, то добавляем его в нашу коллекцию.
if (!elements[0].Equals(thirdLevelNeighbor) && !elements[1].Equals(thirdLevelNeighbor) && !elements[2].Equals(thirdLevelNeighbor)){    elements[3] = thirdLevelNeighbor;    Multiplication multiplication = new Multiplication     {         Elements = elements    };    if (combinations.Where(p=>p.Elements[0].Value==elements[0].Value&& p.Elements[1].Value == elements[1].Value && p.Elements[2].Value == elements[2].Value && p.Elements[3].Value == elements[3].Value).FirstOrDefault()==null)    {        var combination=new Multiplication        {            Elements = new Element[] { elements[0], elements[1], elements[2], elements[3]}        };        combinations.Add(combination);    }}

Работу метода GetFactorCombinations можно визуализировать так:

Теперь перебрав наш двумерный массив ищем самую большую комбинацию чисел.
for (int i = 0; i < 20; i++){    for (int j = 0; j < 20; j++)    {        List<Multiplication> combinations = GetFactorCombinations(i, j);        foreach (var combination in combinations)        {            if (combination.Value > maxMultiplication.Value)            {                Console.WriteLine("Найдена новая самая большая комбинация " + combination.Elements[0].Value + " * " + combination.Elements[1].Value + " * " + combination.Elements[2].Value + " * " + combination.Elements[3].Value + " = " + combination.Value);                maxMultiplication = combination;            }        }    }}Console.WriteLine("Самое большое произведение четырех чисел = " + maxMultiplication.Elements[0].Value + " * " + maxMultiplication.Elements[1].Value + " * " + maxMultiplication.Elements[2].Value + " * " + maxMultiplication.Elements[3].Value + " = " + maxMultiplication.Value);

Если следующая комбинация больше чем maxMultiplication, переписываем его.

Да, мы сделали это. Мы нашли комбинацию из 4 чисел с самым большим произведением.
Фактически, мы решили задачу, но код захардкожен под конкретные условия, чисто процедурный метод. А если нам нужно будет искать из матрицы не 20 на 20, а к примеру 30 на 30 и комбинацию не из 4, а 5 чисел? каждый раз добавлять еще один вложенный цикл (для перебора всех со всеми)
Зарезервируем 2 константы для размера таблицы, и для размера искомой комбинации
const int TABLE_SIZE = 20;public const int COMBINATION_SIZE = 4;

Перепишем метод GetFactorCombinations на рекурсивный метод
static List<Multiplication> GetMultiplicationForStep(int line, int column, List<Element> otherElements = null){    List<Multiplication> resultMultiplications = new List<Multiplication>();    List<Element> resultElements = new List<Element>();     Element element = new Element    {        Column = column,        Line = line,        Value = table[line, column]    };    if (otherElements == null)    {        otherElements = new List<Element>();    }    if(otherElements!=null)    {        resultElements.AddRange(otherElements);    }    if (table[line, column] != 0)    {                        if (otherElements.Where(p => p.Equals(element)).FirstOrDefault() == null)        {            resultElements.Add(element);        }    }    if (resultElements.Count() == COMBINATION_SIZE)    {        Multiplication multiplication = new Multiplication        {            Elements = resultElements.ToArray()        };        resultMultiplications.Add(multiplication);    }    else    {        List<Element> elementNeighbors = FindNeighbors(line, column);        elementNeighbors = elementNeighbors.Where(p => !p.Equals(element)&& otherElements.Where(p=>p.Equals(element)).FirstOrDefault()==null).ToList();        List<Multiplication> newMultiplications = new List<Multiplication>();        foreach(Element neighbor in elementNeighbors)        {            // Если количество комбинаций меньше COMBINATION_SIZE рекурсивно продолжаю искать соседей...            newMultiplications.AddRange(GetMultiplicationForStep(neighbor.Line, neighbor.Column, resultElements).Where(p=>p!=null));        }        foreach(Multiplication multiplication in newMultiplications)        {            if(resultMultiplications.Where(p=>p.Value==multiplication.Value).FirstOrDefault()==null)            {                resultMultiplications.Add(multiplication);            }        }    }    return resultMultiplications;}

Метод работает по тому же принципу как и раньше только вместо вложенных циклов, рекурсивно продолжаем поиск соседей пока количество найденных элементов resultElements.Count() != COMBINATION_SIZE
После нахождения комбинации можно его красиво отобразить в консоли
for (int i = 0; i < TABLE_SIZE; i++){    for (int j = 0; j < TABLE_SIZE; j++)    {        if (maxMultiplication.Elements.Where(p => p.Line == i && p.Column == j).FirstOrDefault() != null)        {            Console.BackgroundColor = ConsoleColor.White;            Console.ForegroundColor = ConsoleColor.Black; // Тут я вывожу таблицу заново, и отображаю самую большую найденную комбинацию            Console.Write(table[i, j] + " ");            Console.ResetColor();        }        else        {            Console.Write(table[i, j] + " ");        }    }    Console.WriteLine();}



Заключение


В этой статье мы познакомились с одним из вариантов нахождения соседних комбинаций в таблице NxN.
Что можно сделать еще:
  • Можно рассмотреть возможность избавиться от множественных повторных переборов всех комбинаций со всеми, и другими способами оптимизировать код.
  • На основе существующего алгоритма реализовать поиск комбинаций соседних чисел на 3-мерном массиве. Но это уже в другой раз

Подробнее..
Категории: Алгоритмы , C , С

Transparent variadic или безысходность стека

26.10.2020 12:15:22 | Автор: admin

Уже пол-шестого утра Ты знаешь, где сейчас твой указатель стека?
Аноним.

Акт I. Сцена I. Интродукция

Вечер. На сервере лениво поскрипывали регистры, то заполняясь данными, то отдавая их обратно. Указатель на стек замедлялся, перемещаясь все медленнее и медленнее, пока не замер совсем. Блин жесткого диска совершил свой последний на сегодня оборот и замер. Сборка проекта завершилась. Долгие 2.5 секунды компиляции завершились и новая версия увидела мир в первый раз.

Однако страдания сервера на этом не закончились. Завершив первую сборку сервер запустил вторую, за ней третью, затем четвертую и пятую. Казалось, что разным версиям не будет конца. Но что поделать, если целевые машины имеют разный набор библиотек; кому-то нужен openssl версией не выше 0.9.8, кому-то непременно нужна MySQL вместо MariaDB. Мир разнообразен и не все готовы менять то, что уже устоялось и работает долгое время.

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

Займет ли автоматизация процесса неделю? Месяц? Год? Нужно ли расширить счетчик потраченных часов в Jira до 64 бит? Кто знает.

Акт I. Сцена II. Основы линковки

Динамическая линковка - есть добрая противоположность линковки статической. Она не создает привязки к версии, она позволяет "на лету" сменить версию библиотеки или поменять подлежащий драйвер работы с базой. Однако это достигается сложностью подключения. Рассмотрим же механизм сей, дабы не говорить больше о нем.

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

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

/* \brief Определим, что мы находимся под Linux */#if defined(__gnu_linux__)#   define OS_GLX#endif/* \brief Определим, что мы находимся под Windows */#if defined(_WIN32) || defined(_WIN64) || \    defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__)#   define OS_WIN#endif

Затем создадим абстракции типов, чтобы впасть в благостное неведение:

#if defined(OS_GLX)/*! \brief Тип для динамической библиотеки */typedef void * library_t;#elif defined(OS_WIN)/*! \brief Тип для динамической библиотеки */typedef HMODULE library_t;#endif

Объявим помощников наших, делающих работу за нас:

/*! \brief Загружает динамическую библиотеку * \param[in] name Имя файла * \return Хэндлер библиотеки */library_t library_load(const char * name) {#if defined(OS_GLX)    return dlopen(name, RTLD_LAZY);#elif defined(OS_WIN)    LPWSTR wname = _stringa2w(name);    library_t lib = LoadLibrary(wname);    free(wname);    return lib;#endif}/*! \brief Получает указатель на функцию из динамической библиотеки * \param[in] lib Библиотека * \param[in] name Имя функции * \return Указатель на функцию */void * library_func(library_t lib, const char * name) {#if defined(OS_GLX)    return dlsym(lib, name);#elif defined(OS_WIN)    return GetProcAddress(lib, name);#endif}/*! \brief Освобождает ресурсы динамической библиотеки * \param[in] lib Библиотека */void library_free(library_t lib) {#if defined(OS_GLX)    dlclose(lib);#elif defined(OS_WIN)    FreeLibrary(lib);#endif}

Теперь же, забыв, где находимся мы можем приступить к загрузке библиотек, дабы одарили они нас функциями их. Для сего необходимо выполнить три действия:

  1. Открыть библиотеку при помощи library_load

  2. Найти нужные функции при помощи library_func

  3. Закрыть библиотеку при помощи library_free

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

Акт I. Сцена III. Избрание

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

$ nm -D /usr/lib/x86_64-linux-gnu/libmysqlclient.so | grep ' T '0000000000033920 T get_tty_password000000000006fa00 T handle_options0000000000061860 T my_init000000000006d830 T my_load_defaults00000000000351e0 T my_make_scrambled_password00000000000220d0 T mysql_affected_rows0000000000025cd0 T mysql_autocommit0000000000020cf0 T mysql_change_user0000000000022160 T mysql_character_set_name0000000000032e10 T mysql_client_find_plugin0000000000032590 T mysql_client_register_plugin000000000002b580 T mysql_close0000000000025c90 T mysql_commit00000000000215c0 T mysql_data_seek0000000000020bb0 T mysql_debug...

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

gcc -E mysql/mysql.h > mysql_generate.h
[...]const char * mysql_stat(MYSQL *mysql);const char * mysql_get_server_info(MYSQL *mysql);const char * mysql_get_client_info(void);unsigned long mysql_get_client_version(void);const char * mysql_get_host_info(MYSQL *mysql);unsigned long mysql_get_server_version(MYSQL *mysql);[...]

Акт I. Сцена IV. Новое обиталище

Подготовим фундамент для нового обиталища заблудших душ:

/* \brief Экземпляр библиотеки */static library_t library = NULL;/* \brief Инициализатор модуля */void _init(void) __attribute__((constructor));void _init(void) {     library = library_load(MYSQL_FILENAME_SO);     if (!library)         // Грусть и уныние         exit(EXIT_FAILURE);}/*! \brief Деинициализатор модуля */void _free(void) __attribute__((destructor));void _free(void) {    if (library)        library_free(library);}

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

const char * mysql_stat(MYSQL * mysql) {    return (const char (*)(MYSQL * mysql))library_func(library, "mysql_stat")(mysql); }const char * mysql_get_server_info(MYSQL * mysql) {         return (const char (*)(MYSQL * mysql))library_func(library, "mysql_get_server_info")(mysql);}

Жизнь была размеренной и спокойной, пока мы не встретили ужас глубин.

Акт II. Сцена I. Вариативная

int mysql_optionsv(MYSQL * mysql,                   enum mysql_option,                   const void * arg,                   ...);

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

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

Даже SWIG, рыцарь из легенд, не cмог побороть монстра, а лишь усыпил его, дав время тому залечить раны да набраться сил.

Уж если сам SWIG не смог, то что делать нам, простым крестьянам, в первый раз взявших в руки вилы? Обратимся к мудрости древних, дабы увидеть, как сей монстр пожирает души врагов своих:

int foo(int argc, ...) {    return argc; }
gcc -S file.c
_Z3fooiz:        pushq   %rbp        movq    %rsp, %rbp        subq    $72, %rsp        movl    %edi, -180(%rbp)        movq    %rsi, -168(%rbp)        movq    %rdx, -160(%rbp)        movq    %rcx, -152(%rbp)        movq    %r8, -144(%rbp)        movq    %r9, -136(%rbp)        testb   %al, %al        je      .L4        movaps  %xmm0, -128(%rbp)        movaps  %xmm1, -112(%rbp)        movaps  %xmm2, -96(%rbp)        movaps  %xmm3, -80(%rbp)        movaps  %xmm4, -64(%rbp)        movaps  %xmm5, -48(%rbp)        movaps  %xmm6, -32(%rbp)        movaps  %xmm7, -16(%rbp).L4:        movl    -180(%rbp), %eax        leave        ret

Ужасен монстр сей. Ведь каждый раз он кладет из регистров данные в стек, чтобы нечестивые дети его - va_start, va_arg, да va_end могли лишь перебирая костяшки стека получать оттуда то, что им не принадлежало.

Акт II. Сцена II. Стековая

Рассмотрим, что же являет собой стек. Стек есть суть массив, в котором данные лежат подряд, однако работа с ними ведется по принципу LIFO. Стек может принять много данных, в отличие от регистров, что быстры подобно стрелам, но малочисленны.

Аргументы функций согласно соглашению о вызове передаются через регистры, а затем через стек (на x86_64), если размера регистров не хватает, чтобы удержать все данные при передаче. Вариативная же функция перекладывает всё содержимое регистров в стек, чтобы все данные, отправленные в функцию кроме регистров находились также и на стеке, давая возможность реализовать va_arg(list, type) как

mov rax, [list.rsp]add list.rsp, sizeof(type)

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

Так как же нам заставить машину передать эти данные в следующую функцию? Ведь мы не можем написать вот так:

int foo(int argc, ...) {    return bar(argc, ...); }

В некоторых случаях мы могли бы ориентироваться на аргументы, передаваемые с функцией - argc может означать количество аргументов, но не говорит об их типе или размере. NULL-терминированный список аргументов можно перепутать с NULL-параметром в списке аргументов, строковые форматтеры сложны в реализации и имеют много подводных камней.

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

Акт II. Сцена III. Бездна-void

Мы видели, что происходит внутри такой функции. Но что происходит снаружи?

int foo(int argc, ...) {    return argc;}void bar(void) {    foo(1, 2, 3, "string");}
gcc -S file.c
.LC0:        .string "string"_Z3barv:        pushq   %rbp        movq    %rsp, %rbp        movl    $.LC0, %ecx        movl    $3, %edx        movl    $2, %esi        movl    $1, %edi        movl    $0, %eax        call    _Z3fooiz

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

Если в нашей реализации proxy-функции мы возьмем из стека достаточное количество байт и передадим их в симулируемую функцию обычным образом, то она не заметит подмены:

int foo(int argc, ...) {    return realfoo(argc, <байты из стека>); }

Какие же инструменты мы можем применить, чтобы это сделать? В первую очередь мы могли бы работать со стеком напрямую, например через получения указателя на argc и последовательным разыменованием его после прибавления к нему размера машинного слова.

int foo(int argc, ...) {    long long int * rsp = &argc;    return realfoo(argc, *(rsp + 1), *(rsp + 2), <...>);}

Однако такой способ неудобен и неочевиден и нас проклянут все последующие поколения, кто будет читать сей опус. Поэтому мы будем использовать стандартизированные инструменты доступа к стеку - те самые функции va:

#include <stdio.h>#include <stdarg.h>void params(const char * fmt, ...) {    va_list list;    va_start(list, fmt);    vprintf(fmt, list);    va_end(list);}void wrapper(const char * fmt, ...) {    va_list list;    va_start(list, fmt);        void * v1 = va_arg(list, void *);    void * v2 = va_arg(list, void *);    void * v3 = va_arg(list, void *);    void * v4 = va_arg(list, void *);    void * v5 = va_arg(list, void *);    void * v6 = va_arg(list, void *);    void * v7 = va_arg(list, void *);    void * v8 = va_arg(list, void *);    void * v9 = va_arg(list, void *);        params(fmt, v1, v2, v3, v4, v5, v6, v7, v8, v9);        va_end(list); }int main(void) {    params("%d %s %lld\n", 5, "sss", (long long int) 32);    wrapper("%d %s %lld\n", 5, "sss", (long long int) 32);    return 0;}

Но следует обратить внимание, что вставив va_arg сразу в params мы отдадим порядок их вызова на откуп хитрому компилятору, что сломает порядок чтения.

Этот способ работает как на x86_64, так и на x86 архитектурах. Если по каким-то причинам нам не будет хватать 9 параметров мы всегда сможем поместить v-переменные в массив нужного нам размера и воспользоваться циклом. Чтение из стека "лишних" переменных ни на что не влияет, поскольку в этой же функции стек был уже заполнен "мусорными" регистрами при входе в эту функцию.

Прочитать же, например, большую структуру через va_arg и затем поместить ее в params тоже не выйдет, ибо как не помещающаяся в регистр она будет передана через стек.

После вызова функции wrapper переменные забираются из стека, и снова кладутся в регистры, после чего вызывается функция params, которая не замечая, что переменные были преобразованы в void * выведет их так же, как и в первый раз.

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

Акт II. Сцена IV. Эпилог

Блин диска повернулся еще раз и остановился. Собрав проект один раз, сервер остановился, тяжело вздохнул всеми кулерами, и задумался о том, понадобится ли он еще когда-нибудь или это была последняя сборка в его жизни. Нужен ли он еще или в коде достигнут идеал? Можно ли вообще достигнуть идеала?

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

Подробнее..

Книга C 8 и .NET Core. Разработка и оптимизация

15.04.2021 12:10:33 | Автор: admin
image Привет, Хаброжители! В издании рассмотрены все темы, связанные с разработкой на C#. В начале книги вы ознакомитесь с основами C#, в том числе с объектно-ориентированным программированием, а также с новыми возможностями C# 8.0. Несколько глав посвящено .NET Standard API, применяемым для запроса данных и управления ими, отслеживания производительности и ее повышения, работы с файловой системой, асинхронными потоками, сериализацией и шифрованием. Кроме того, на примерах кроссплатформенных приложений вы сможете собрать и развернуть собственные. Например, веб-приложения с использованием ASP.NET Core или мобильные приложения на Xamarin Forms.

Также вы познакомитесь с технологиями, применяемыми при создании приложений Windows для ПК, в частности с Windows Forms, Windows Presentation Foundation (WPF) и Universal Windows Platform (UWP).

Улучшение производительности и масштабируемости с помощью многозадачности


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

В этой главе:

  • процессы, потоки и задачи;
  • мониторинг производительности и использования ресурсов;
  • асинхронное выполнение задач;
  • синхронизация доступа к общим ресурсам;
  • ключевые слова async и await.

Процессы, потоки и задачи


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

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

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

Потоки имеют свойства Priority и ThreadState. Кроме того, существует класс ThreadPool, предназначенный для управления пулом фоновых рабочих потоков, как показано на следующей схеме (рис. 13.1).

image

Если вы как разработчик имеете дело со сложными действиями, которые должны быть выполнены вашим кодом, и хотите получить полный контроль над ними, то можете создавать отдельные экземпляры класса Thread и управлять ими. При наличии одного основного потока и нескольких небольших действий, которые можно выполнять в фоновом режиме, вы можете добавить экземпляры делегатов, указывающие на эти фрагменты, реализованные в виде методов в очередь, и они будут автоматически распределены по потокам с помощью пула потоков.
Дополнительную информацию о пуле потоков можно получить на сайте docs.microsoft.com/ru-ru/dotnet/standard/threading/the-managed-thread-pool.

Потокам, возможно, придется конкурировать и ждать доступа к общим ресурсам, таким как переменные, файлы и объекты баз данных.

В зависимости от задачи удвоение количества потоков (рабочих) не уменьшает вдвое количество секунд, которое будет затрачено на выполнение задачи. Фактически это может даже увеличить длительность выполнения задачи, как показано на рис. 13.2.

image

Никогда не полагайтесь на то, что увеличение количества потоков повысит производительность! Выполните тесты производительности сначала базового кода без реализации нескольких потоков, а затем кода с несколькими потоками. Выполняйте тесты производительности в промежуточной среде, максимально приближенной к рабочей.

Мониторинг производительности и использования ресурсов


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

Оценка эффективности типов


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

  • функциональность для принятия решения проверяется, предоставляет ли данный тип необходимую вам функциональность;
  • объем памяти для принятия решения проверяется, сколько байтов памяти потребляет данный тип;
  • производительность для принятия решения проверяется, насколько быстро работает данный тип;
  • потребности в будущем этот фактор зависит от возможных требований и возможности поддержки.

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

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

Если же необходимо сохранить только несколько чисел, но выполнить с ними большое количество операций, то лучше выбрать тип, который быстрее всего работает на конкретном ЦПУ.

Ранее упоминалась функция sizeof(), предназначенная для отображения количества байтов, занимаемых в памяти одним экземпляром конкретного типа. При сохранении большого количества значений в более сложных структурах данных, таких как массивы и списки, измерение использования памяти требует способа получше.

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

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

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

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

Мониторинг производительности и использования памяти


В пространстве имен System.Diagnostics реализовано большое количество полезных типов для мониторинга вашего кода. В первую очередь следует рассмотреть тип Stopwatch.

1. Создайте в папке Code папку Chapter13 с двумя подпапками MonitoringLib и MonitoringApp.

2. В программе Visual Studio Code сохраните рабочую область как Chapter13.code-workspace.

3. Добавьте в рабочую область папку MonitoringLib, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект библиотеки классов, как показано в следующей команде:

dotnet new classlib

4. Добавьте в рабочую область папку MonitoringApp, откройте для нее новую панель TERMINAL (Терминал) и создайте новый проект консольного приложения, как показано в следующей команде:

dotnet new console

5. В проекте MonitoringLib переименуйте файл Class1.cs на Recorder.cs.

6. В проекте MonitoringApp найдите и откройте файл MonitoringApp.csproj и добавьте ссылку на библиотеку MonitoringLib, как показано ниже (выделено полужирным шрифтом):

<Project Sdk="Microsoft.NET.Sdk">   <PropertyGroup>      <OutputType>Exe</OutputType>      <TargetFramework>netcoreapp3.0</TargetFramework>   </PropertyGroup>   <ItemGroup>      <ProjectReference          Include="..\MonitoringLib\MonitoringLib.csproj" />      </ItemGroup></Project>

7. На панели TERMINAL (Терминал) скомпилируйте проекты, как показано в следующей команде:

dotnet build

Реализация класса Recorder


Тип Stopwatch содержит несколько полезных членов, как показано в табл. 13.1.

image

Тип Process содержит несколько полезных членов, перечисленных в табл. 13.2.

image

Для реализации класса Recorder мы будем использовать классы Stopwatch и Process.

1. Откройте файл Recorder.cs и измените его содержимое, чтобы задействовать экземпляр класса Stopwatch в целях записи времени и текущий экземпляр класса Process для записи использованной памяти, как показано ниже:

using System;using System.Diagnostics;using static System.Console;using static System.Diagnostics.Process;namespace Packt.Shared{   public static class Recorder{   static Stopwatch timer = new Stopwatch();   static long bytesPhysicalBefore = 0;   static long bytesVirtualBefore = 0;   public static void Start()   {      // очистка памяти, на которую больше нет ссылок,      // но которая еще не освобождена     GC.Collect();     GC.WaitForPendingFinalizers();     GC.Collect();     // сохранение текущего использования физической     // и виртуальной памяти     bytesPhysicalBefore = GetCurrentProcess().WorkingSet64;     bytesVirtualBefore = GetCurrentProcess().VirtualMemorySize64;     timer.Restart();   }   public static void Stop()   {     timer.Stop();     long bytesPhysicalAfter = GetCurrentProcess().WorkingSet64;     long bytesVirtualAfter =      GetCurrentProcess().VirtualMemorySize64;    WriteLine("{0:N0} physical bytes used.",       bytesPhysicalAfter - bytesPhysicalBefore);    WriteLine("{0:N0} virtual bytes used.",      bytesVirtualAfter - bytesVirtualBefore);    WriteLine("{0} time span ellapsed.", timer.Elapsed);     WriteLine("{0:N0} total milliseconds ellapsed.",       timer.ElapsedMilliseconds);   } }}

В методе Start класса Recorder используется сборщик мусора (garbage collector, класс GC), позволяющий нам гарантировать, что вся выделенная в настоящий момент память будет собрана до записи количества использованной памяти. Это сложная техника, и ее стоит избегать при разработке прикладной программы.

2. В классе Program в метод Main добавьте операторы для запуска и остановки класса Recorder при генерации массива из 10 000 целых чисел, как показано ниже:

using System.Linq;using Packt.Shared;using static System.Console;namespace MonitoringApp{    class Program    {      static void Main(string[] args)      {        WriteLine("Processing. Please wait...");        Recorder.Start();        // моделирование процесса, требующего ресурсов памяти...        int[] largeArrayOfInts =           Enumerable.Range(1, 10_000).ToArray();        // ...и занимает некоторое время, чтобы завершить        System.Threading.Thread.Sleep(           new Random().Next(5, 10) * 1000);        Recorder.Stop();      }   }}

3. Запустите консольное приложение и проанализируйте результат:

Processing. Please wait...655,360 physical bytes used.536,576 virtual bytes used.00:00:09.0038702 time span ellapsed.9,003 total milliseconds ellapsed

Измерение эффективности обработки строк

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

1. Закомментируйте предыдущий код в методе Main, обернув его символами /* и */.

2. Добавьте в метод Main следующий код. Он создает массив из 50 000 переменных int, а затем конкатенирует их, используя в качестве разделителей запятые, с помощью классов string и StringBuilder:

int[] numbers = Enumerable.Range(1, 50_000).ToArray();Recorder.Start();WriteLine("Using string with +");string s = "";for (int i = 0; i < numbers.Length; i++){   s += numbers[i] + ", ";}Recorder.Stop();Recorder.Start();WriteLine("Using StringBuilder");var builder = new System.Text.StringBuilder();for (int i = 0; i < numbers.Length; i++){   builder.Append(numbers[i]); builder.Append(", ");}Recorder.Stop();

3. Запустите консольное приложение и проанализируйте результат:

Using string with +11,231,232 physical bytes used.29,843,456 virtual bytes used.00:00:02.6908216 time span ellapsed.2,690 total milliseconds ellapsed.Using StringBuilder4,096 physical bytes used.0 virtual bytes used.00:00:00.0023091 time span ellapsed.2 total milliseconds ellapsed.

Исходя из результатов, мы можем сделать следующие выводы:

  • класс string вместе с оператором + использовал около 11 Мбайт физической памяти, 29 Мбайт виртуальной и занял по времени 2,7 с;
  • класс StringBuilder использовал 4 Кбайт физической памяти, 0 виртуальной и занял менее 2 мс.

В нашем случае при конкатенации текста класс StringBuilder выполняется примерно в 1000 раз быстрее и приблизительно в 10 000 раз эффективнее по затратам ресурсов памяти!
Избегайте использования метода String.Concat и оператора + внутри цикла. Вместо этого для конкатенации переменных, особенно в циклах, применяйте класс StringBuilder.

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

Об авторе


imageМарк Дж. Прайс обладатель сертификатов Microsoft Certified Trainer (MCT), Microsoft Specialist: Programming in C# и Microsoft Specialist: Architecting Microsoft Azure Infrastructure Solutions. За его плечами более 20 лет практики в области обучения и программирования.

С 1993 года Марк сдал свыше 80 экзаменов корпорации Microsoft по программированию и специализируется на подготовке других людей к успешному прохождению тестирования. Его студенты как 16-летние новички, так и профессионалы с многолетним опытом. Марк ведет эффективные тренинги, делясь реальным опытом консультирования и разработки систем для корпораций по всему миру.

В период с 2001 по 2003 год Марк посвящал все свое время разработке официального обучающего программного обеспечения в штаб-квартире Microsoft в американском городе Редмонд. В составе команды он написал первый обучающий курс по C#, когда была только выпущена ранняя альфа-версия языка. Во время сотрудничества с Microsoft он преподавал на курсах повышения квалификации сертифицированных корпорацией специалистов, читая лекции по C# и .NET.

В настоящее время Марк разрабатывает и поддерживает обучающие курсы для системы Digital Experience Platform компании Episerver, лучшей .NET CMS в сфере цифрового маркетинга и электронной коммерции.

В 2010 году Марк получил свидетельство об окончании последипломной программы обучения, дающее право на преподавание. Он преподает старшеклассникам математику в двух средних школах в Лондоне. Кроме того, Марк получил сертификат Computer Science BSc. Hons. Degree в Бристольском университете (Англия).

О научном редакторе


Дамир Арх профессионал с многолетним опытом разработки и сопровождения различного программного обеспечения: от сложных корпоративных программных проектов до современных потребительских мобильных приложений. Он работал со многими языками, однако его любимым остается C#. Стремясь к совершенствованию процессов, Дамир предпочитает разработку, основанную на тестировании, непрерывной интеграции и непрерывном развертывании. Он делится своими знаниями, выступая на конференциях, ведет блоги и пишет статьи. Дамир Арх семь раз подряд получал престижную премию Microsoft MVP за разработку технологий. В свободное время он всегда в движении: любит пеший туризм, геокэшинг, бег и скалолазание.

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону .NET

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru