Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
  SQL 30.07   
  Python 10.06   
 
TOP 20
 Alg3...2082 
 Intel 386...780 
 Secure Programming for Li...655 
 Trees...651 
 Си за 21 день...565 
 Ethreal 1...551 
 Стивенс 1...550 
 Lists...546 
 2.0-> Linux IP Networking...544 
 Steve Pate 3...535 
 Stein-MacEachern-> Час...532 
 Ethreal 2...517 
 Стивенс 4...484 
 Python...481 
 Rodriguez 6...477 
 Advanced Bash Scripting G...451 
 William Gropp...451 
 Стивенс 5...424 
 Keogh 2...404 
 Комментарий...394 
 
  01.08.2020 : 2947670+ посещений 

iakovlev.org

Оптимизация

Оптимизация кода с использованием GNU C Compiler

By Rahul U Joshi

Эта статья описывает технику оптимизации кода для GNU C Compiler. Компилятор - это программа , которая читает исходный код и транслирует его в машинные команды . Этот процесс можно разбить на несколько стадий , одной из которых является оптимизация результирующего кода. GNU C Compiler использует большой массив методов оптимизации.

Asm-код для C программы

GCC берет исходный С-код и производит из него бинарный машинный код . Его также можно заставить сгенерировать читабельный промежуточный asm-код . Для генерации такого asm-кода создадим файл test1.c и выполним команду :
 	$ gcc -c -S test1.c
Будет сгенерирован файл test1.s
 Код :
 	6 : #include <stdio.h>
 	7 : 
 	8 : int main()
 	9 : {
 	10 :     printf("Hello, World\n");
 	11 :     return 0;
 	12 :}
 	13 :
 	14 : /* end test1.c */
 	15 : /* ---------------------------------------------------- */
 	16 : /* generated assembly language file */
 	17 :     .file   "test1.c"    /* some assembler directives to be */
 	18 :     .version    "01.01" /* ignored */
 	19 : gcc2_compiled.:
 	20 : .section    .rodata    /* this segment has read-only data */
 	21 : .LC0:
 	22 :     .string "Hello, World\n"
 	23 : .text
 	24 :     .align 4
 	25 : .globl main
 	26 :     .type    main,@function
 	27 : main:                           /* main function begins */
 	28 :     pushl $.LC0      /* помещаем параметр для printf() в стек */
 	29 :     call printf                 /* вызываем функцию */
 	30 :     addl $4,%esp                /* очищаем стек */
 	31 : 
 	32 :     xorl  %eax,%eax  /* обнуляем EAX = 0, functions use register */
 	33 :     jmp .L1                     /* EAX to return values */
 	34 :     .p2align 4,,7               /* выравнивание */
 	35 : .L1:
 	36 :     ret                         /* return from main, done */
 	37 : .Lfe1:
 	38 :     /* пропускаем */
 	39 :     .size    main,.Lfe1-main        
 	40 :     .ident  "GCC: (GNU) egcs-2.91.66 (egcs-1.1.2 release)"
 	41 : /* конец */
 	42 : /* -------------------------------------------------------- */
 		
Линуксовый компилятор генерит код с AT&T-синтаксисом , который отличается от Intel/Microsoft Assembler/Turbo Assembler-синтаксиса. Некоторые директивы мы опустили . В 20-й строке инициализируется сегмент данных на чтение , в котором хранится строка "Hello,World\n" . Этой строке была присвоена метка .LC0 . В 27-й строке начинается код для функции main(). Параметры функции передаются через стек в обратном порядке , т.е. от последнего к первому. Для printf() таким параметром будет строка "Hello, World\n". Строка pushl $.LC0 размещает адрес строки "Hello, World\n" в стек. Префикс "l" в команде pushl говорит о типе "long" для 32-битной строки . Повторений этого префикса дя других команд будет означать то же самое. Префикс "$" перед .LC0 означает адрес строки . Следующий оператор вызывает вызов функции printf() . После ее выполнения нужно очистить стек , для этого мы к регистру ESP прибавляем 4 , т.к. адрес строки был 4-битный. Стандартный интеловский синтаксис имеет формат инструкций <instruction> dest, src. А в строке 30 мы видим , что у AT&T все наоборот - <instruction> src, dest. Первый операнд этой команды идет с префиксом "доллар" , второй - с префиксом "процент". Это сделано специально для совместимости с BSD-ассемблером. Следующий оператор XOR обнуляет регистр EAX = 0 . И т.д.

Constant Folding

Ну а теперь приступим собственно к оптимизации. Термин "Constant folding" - пример простейшей оптимизации . Предположим в С программе есть выражение x = 45 * 88; . Неоптимизированный код добросовестно перемножит эти 2 числа . А оптимизатор обнаружит , что и 45 и 88 - это констаны , и результат константа , поэтому он на этапе компиляции заранее вычислит результат и скопирует его в х . Это и есть constant folding. В качестве иллюстрации приводим код test2.c :
  1 : /* test2.c */
  2 : /* Demonstration of constant propagation */
  3 : #include <stdio.h>
  4 :
  5 : int main()
  6 : {
  7 :     int x, y, z;
  8 :     x = 10;
  9 :     y = x + 45;
 10 :     z = y + 4;
 11 :     printf("The value of z = %d", z);
 12 :     return 0;
 13 : }
 14 : /* end of test2.c */
 15 : 
 16 : /* ------------------------------------------- */
 17 : /* assembly language file без оптимизации */
 18 :     .file   "test2.c"
 19 :     .version    "01.01"
 20 : gcc2_compiled.:
 21 : .section    .rodata
 22 : .LC0:
 23 :     .string "The value of z = %d"
 24 : .text
 25 :     .align 4
 26 : .globl main
 27 :     .type    main,@function
 28 : main:
 29 :     pushl %ebp             /* save EBP register on stack */
 30 :     movl %esp,%ebp         /* EBP = ESP */
 31 :     subl $12,%esp          /* Create stack frame. 3 variables x 4 bytes */
 32 :
 33 :     /* x = 10; */
 34 :     movl $10,-4(%ebp)      /* x = 10. x is topmost on the stack */
 35 : 
 36 :     /* y = x + 45; */
 37 :     movl -4(%ebp),%edx     /* EDX = x */
 38 :     addl $45,%edx          /* EDX = EDX + 45 */
 39 :     movl %edx,-8(%ebp)     /* y = EDX. y is second from top of stack */
 40 : 
 41 :     /* z = y + 4 */
 42 :     movl -8(%ebp),%edx     /* EDX = y */
 43 :     addl $4,%edx           /* EDX = EDX + 4 */
 44 :     movl %edx,-12(%ebp)    /* z = EDX. z is third from top of stack */
 45 : 
 46 :     /* printf("The value of z = ", z); */
 47 :     movl -12(%ebp),%eax    /* EAX = z */
 48 :     pushl %eax             /* push EAX(=z) as first parameter of printf */
 49 :     pushl $.LC0            /* second parameter for printf */
 50 :     call printf                 
 51 :     addl $8,%esp           /* clear stack */
 52 : 
 53 :     /* return 0; */
 54 :     xorl %eax,%eax         /* for return 0 */
 55 :     jmp .L1
 56 :     .p2align 4,,7
 57 : .L1:
 58 :     leave
 59 :     ret
 60 : .Lfe1:
 61 :     .size    main,.Lfe1-main
 62 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 63 : /* end of assembly language code */
 64 : /* ---------------------------------------------------------------- */
 65 :
 66 : /* ---------------------------------------------------------------- */
 67 : /* оптимизированный asm-код*/
 68 : 
 69 :     .file   "test2.c"
 70 :     .version    "01.01"
 71 : gcc2_compiled.:
 72 : .section    .rodata
 73 : .LC0:
 74 :     .string "The value of z = %d"
 75 : .text
 76 :     .align 4
 77 : .globl main
 78 :     .type    main,@function
 79 : main:
 80 :     pushl %ebp             /* Save EBP register on stack */
 81 :     movl %esp,%ebp         /* EBP = ESP */
 82 : 
 83 :     /* by constant propagation, z will always be 59 */
 84 :     /* printf("The value of z = %d", z); */
 85 :     pushl $59              /* first printf parameter */
 86 :     pushl $.LC0            /* second printf parameter */
 87 :     call printf
 88 :                            /* no need of cleanup, we are exiting anyway */
 89 :     /* return 0; */
 90 :     xorl %eax,%eax              
 91 :     leave
 92 :     ret
 93 : .Lfe1:
 94 :     .size    main,.Lfe1-main
 95 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 96 : /* end of assembly language code */
 97 : /* ----------------------------------------------------------------- */
 
В главной функции определяются 3 локальных переменных , которые размещаются в стеке. Для доступа к ним будет использована индексная адресация с помощью EBP . Для этого в него копируется указатель стека ESP . Из ESP мы вычитаем 12 = 3*4 байт, отчего размер стека увеличивается на 12 байт. Диаграмма стека :
              EBP-----> ---------------
                        |      x      |       4 Bytes
                        ---------------
                        |      y      |       4 Bytes
                        ---------------
                        |      z      |       4 Bytes
              ESP-----> ---------------
                        |             |
                        |             |
                    ^   |             |
                    |         ...
 Адресация растет   |   |             |
 снизу вверх        | 0 ---------------
 
Теперь рассмотрим операцию x = 10;. Оно реализовано в 34-й строке выражением movl $10, -4(%ebp). Индексная адресация записана в форме <offset>(<base register>). 10 копируется в стек по адресу (EBP - 4) т.е. по адресу x . По аналогии, -8(%ebp) используется для доступа к y и -12(%ebp) для доступа к z. Линии 37, 38 и 39 вычисляют x + 45 и присваивают y. Для этого используетс EDX . Аналогично для z = y + 4 is similar. В 47 параметры для printf() передаются в стек. Последний параметр (z) ложится в стек первым . В 51 строке стек очищается . Как видно из вычислений , результат для y всегда равен 55 , а для z = 59 . Оптимизатор в состоянии это определить . Для разрешения оптимизации нужно использовать опцию -O2 :
     gcc -c -S -O2 test2.c
Оптимизированный код показан в строках с 69 по 95. В этом куске отсутствуют переменные x,y,x .

Бывает , что одно и тоже выражение с одним и тем же результатом вычисляется несколько раз . Компилятор оптимизирует такие случаи с помощью
common subexpression elimination .
Рассмотрим следующий пример :
  1 : /* test3.c */
  2 : /* common subexpression elimination, and also of constant propagation */
  3 : #include <stdio.h>
  4 :
  5 : int main()
  6 : {
  7 :     int a, b;
  8 :     int x, y, z;
  9 :     scanf("%d %d", &a, &b);
 10 :     x = a * b;
 11 :
 12 :     if(b >= 4)
 13 :     {
 14 :         y = a * b;
 15 :         z = 0;
 16 :     }
 17 :     else
 18 :     {
 19 :         z = a * b * 4;
 20 :         y = 0;
 21 :     }
 22 :
 23 :     printf("x = %d, y = %d, z = %d\n", x, y, z);
 24 :     return 0;
 25 : }
 26 : /* end of test3.c */
 27 :
 28 : /* ----------------------------------------------------------------- */
 29 : /* generated unoptimized assembly language code */
 30 :     .file   "test3.c"
 31 :     .version    "01.01"
 32 : gcc2_compiled.:
 33 : .section    .rodata
 34 : .LC0:
 35 :     .string "%d %d"
 36 : .LC1:
 37 :     .string "x = %d, y = %d, z = %d\n"
 38 : .text
 39 :     .align 4
 40 : .globl main
 41 :     .type    main,@function
 42 : main:
 43 :     pushl %ebp                  /* save EBP */
 44 :     movl %esp,%ebp              /* EBP = ESP */
 45 :     subl $20,%esp               /* Create space for 5 variables */
 46 :
 47 :     /* scanf("%d %d". &a, &b); */
 48 :     leal -8(%ebp),%eax
 49 :     pushl %eax                  /* push address of b on stack */
 50 :     leal -4(%ebp),%eax
 51 :     pushl %eax                  /* push address of a on stack */
 52 :     pushl $.LC0                 /* push address of string */
 53 :     call scanf
 54 :     addl $12,%esp               /* stack cleanup after scanf */
 55 : 
 56 :     /* x = a * b; */
 57 :     movl -4(%ebp),%eax          /* EAX = a */
 58 :     imull -8(%ebp),%eax         /* EAX = EAX * b = a * b */
 59 :     movl %eax,-12(%ebp)         /* x = EAX = a * b */
 60 : 
 61 :     /* if( b >= 4)... */
 62 :     cmpl $3,-8(%ebp)            /* compare b with 3 */
 63 :     jle .L2                     /* else part at label .L2, if follows */
 64 : 
 65 :     /* y = a * b; */
 66 :     movl -4(%ebp),%eax          /* EAX = a */
 67 :     imull -8(%ebp),%eax         /* EAX = EAX * b = a * b */
 68 :     movl %eax,-16(%ebp)         /* y = EAX = a * b */
 69 :     /* z = 0; */
 70 :     movl $0,-20(%ebp)
 71 :     jmp .L3                     /* jump over the else part */
 72 : 
 73 :     .p2align 4,,7
 74 : .L2:
 75 :     /* else part begins here */
 76 :
 77 :     /* z = a * b * 4; */
 78 :     movl -4(%ebp),%eax          /* EAX = a */
 79 :     imull -8(%ebp),%eax         /* EAX = EAX * b = a * b */
 80 :     leal 0(,%eax,4),%edx        /* EDX = EAX*4 + 0 */
 81 :     movl %edx,-20(%ebp)         /* z = EDX */
 82 :     /* y = 0; */
 83 :     movl $0,-16(%ebp)
 84 : .L3:
 85 :     /* if..else is over here */
 86 : 
 87 :     /* printf("x = %d, y = %d, z = %d\n", x, y, x); */
 88 :     movl -20(%ebp),%eax
 89 :     pushl %eax                  /* push address of z on stack */
 90 :     movl -16(%ebp),%eax
 91 :     pushl %eax                  /* push address of y on stack */
 92 :     movl -12(%ebp),%eax
 93 :     pushl %eax                  /* push address of x on stack */
 94 :     pushl $.LC1                 /* address of string */
 95 :     call printf
 96 :     addl $16,%esp               /* stack cleanup after printf */
 97 : 
 98 :     /* return 0 */
 99 :     xorl %eax,%eax
 100 :     jmp .L1
 101 :     .p2align 4,,7
 102 : .L1:
 103 :     leave
 104 :     ret
 105 : .Lfe1:
 106 :     .size    main,.Lfe1-main
 107 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 108 : /* end of unoptimized assembly language code */
 109 : /* --------------------------------------------------------------- */
 110 : 
 111 : /* --------------------------------------------------------------- */
 112 : /* generated optimized assembly language code */
 113 :     .file   "test3.c"
 114 :     .version    "01.01"
 115 : gcc2_compiled.:
 116 : .section    .rodata
 117 : .LC0:
 118 :     .string "%d %d"
 119 : .LC1:
 120 :     .string "x = %d, y = %d, z = %d\n"
 121 : .text
 122 :     .align 4
 123 : .globl main
 124 :     .type    main,@function
 125 : main:
 126 :     pushl %ebp               /* save EBP */
 127 :     movl %esp,%ebp           /* EBP = ESP */
 128 :     subl $8,%esp             /* space for just 2 variables on stack */
 129 :
 130 :     /* scanf("%d %d", &a, &b); */
 131 :     leal -4(%ebp),%eax
 132 :     pushl %eax               /* push address of b on stack */
 133 :     leal -8(%ebp),%eax
 134 :     pushl %eax               /* push address of a on stack */
 135 :     pushl $.LC0              /* address of string */
 136 :     call scanf
 137 : 
 138 :     /* x = a * b; */
 139 :     movl -4(%ebp),%eax       /* EAX = b */
 140 :     movl %eax,%edx           /* EDX = EAX = b */
 141 :     imull -8(%ebp),%edx      /* EDX = EDX * a = b * a = a * b */
 142 :
 143 :     addl $12,%esp            /* delayed stack cleanup */
 144 :     /* if( b >= 4).... */
 145 :     cmpl $3,%eax             /* compare EAX = b with 3 */
 146 :     jle .L17                 /* else part from label .L17 */
 147 : 
 148 :                              /* y stored in ECX, z in EAX, x in EDX */   
 149 :     /* y = a * b; */
 150 :     movl %edx,%ecx
 151 :     /* z = 0; */
 152 :     xorl %eax,%eax
 153 :     jmp .L18                 /* jump over the else part */
 154 :     .p2align 4,,7
 155 : .L17:
 156 :     /* z = a * b * 4; */
 157 :     leal 0(,%edx,4),%eax     /* LEA EAX, [EDX*4]+0 */
 158 :     /* y = 0; */
 159 :     xorl %ecx,%ecx
 160 : .L18:
 161 :     pushl %eax               /* push value of z */
 162 :     pushl %ecx               /* push value of y */
 163 :     pushl %edx               /* push value of x */
 164 :     pushl $.LC1              /* push address of string */
 165 :     call printf
 166 :     /* stack cleanup after printf not necessary */
 167 :
 168 :     /* return 0; */
 169 :     xorl %eax,%eax
 170 :     leave
 171 :     ret
 172 : .Lfe1:
 173 :     .size    main,.Lfe1-main
 174 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 175 : /* end optimized assembly code */
 176 : /* -------------------------------------------------------------- */
 
В данном примере одно и то же вычисление a * b делается несколько раз . Код с 30 по 108 неоптимизирован и легко читаем . Оптимизированный код представлен с 113 по 176 строки . В стеке хранятся 2 переменные - a и b , поскольку их нельзя положить в регистр . Для других переменных компилятор выбирает регистры : x в EDX, y в ECX и z в EAX. Результат a * b хранится в EDX используется комбинация movl, imull :
     movl %edx, %eax         /* EAX = EDX  = a * b */
     imull $4, %eax          /* EAX = EAX * 4 */
 Также используется сдвиг (sall) вместо imull
 Также для оптимизации используется
     leal 0(,%edx,4), %eax
Последняя инструкция использует фичи 80386-го процессора : регистр EDX выполняет роль индексного регистра, 4 - масштаб и 0 - смешение,здесь вычисляется эфеективный адрес и хранится в EAX-регистре . Регистр EAX получает значение EDX(=a*b)*4 + 0 , т.е. a * b * 4. Инструкция leal выполняется за 2 цикла , в то время как сдвиг - за 7 циклов .

Удаление неиспользуемого кода

Это такой код в программе , который никогда не выполняется . Например , если условие if никогда не выполняется , компилятор просто удалит этот код . Cледующий пример это демонстрирует :
  1 : /* test4.c */
  2 : /* demonstration of dead code elimination */
  3 : #include <stdio.h>
  4 :
  5 : int main()
  6 : {
  7 :     int x;
  8 :
  9 :     scanf("%d", &x);
 10 :
 11 :     if(x < 0 && x > 0)
 12 :     {
 13 :         x = 99;
 14 :         printf("Hello. Inside the if!!!");
 15 :     }
 16 :
 17 :     return 0;
 18 : }
 19 : /* end of test4.c */
 20 :
 21 : /* --------------------------------------------------------------- */
 22 : /* optimized assembly code */
 23 :     .file   "test4.c"
 24 :     .version    "01.01"
 25 : gcc2_compiled.:
 26 : .section    .rodata
 27 : .LC0:
 28 :     .string "%d"
 29 : .LC1:
 30 :     .string "Hello. Inside the if!!!"
 31 : .text
 32 :     .align 4
 33 : .globl main
 34 :     .type    main,@function
 35 : main:
 36 :     pushl %ebp             /* save EBP on stack */
 37 :     movl %esp,%ebp         /* EBP = ESP */
 38 :     subl $4,%esp           /* create space for x on the stack */
 39 :
 40 :     /* scanf("%d", &x); */
 41 :     leal -4(%ebp),%eax
 42 :     pushl %eax             /* push address of a on stack */
 43 :     pushl $.LC0            /* push string on stack */
 44 :     call scanf
 45 :
 46 :     /* the entire body of the if and the condition checking is dead code */
 47 :     /* return 0; */
 48 :     xorl %eax,%eax         /* no stack cleanup, we are exiting anyway */
 49 :     leave
 50 :     ret
 51 : .Lfe1:
 52 :     .size    main,.Lfe1-main
 53 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 54 : /* end optimized assembly code */
 55 : /* ---------------------------------------------------------------- */
 
В коде условие в строке 11 есть x < 0 && x > 0,которое абсурдно . Компилятор не будет его генерировать .

Strength Reduction using Induction Variable

Если например в цикле многократно выполняется вычисление x2 , имеет смысл вызову экспоненциальной функции предпочесть простое умножение x на x . Такие переменные называются induction variable.
 Пример : 
  1 : /* test5.c */
  2 : /* demonstration of induction variable elimination */
  3 : int main()
  4 : {
  5 :     int i, j;
  6 :
  7 :     for(i = 0; i < 10; i++)
  8 :     {
  9 :         j = i * 7;
 10 :         printf("i = %d, j = %d", i, j);
 11 :     }
 12 :     return 0;
 13 : }
 14 : /* end test5.c */
 15 :
 16 : /* --------------------------------------------------------------- */
 17 : /* optimized assembly language code */
 18 :     .file   "test5.c"
 19 :     .version    "01.01"
 20 : gcc2_compiled.:
 21 : .section    .rodata
 22 : .LC0:
 23 :     .string "i = %d, j = %d"
 24 : .text
 25 :     .align 4
 26 : .globl main
 27 :     .type    main,@function
 28 : main:
 29 :     pushl %ebp                  /* save EBP on stack */
 30 :     movl %esp,%ebp              /* ESP = EBP */
 31 :
 32 :     pushl %esi                  /* ESI will hold 'j' */
 33 :     pushl %ebx                  /* EBX will hold 'i' */
 34 :     xorl %ebx,%ebx              /* both are initialized to zero */
 35 :     xorl %esi,%esi
 36 :     .p2align 4,,7
 37 : .L5:
 38 :     /* printf("i = %d, j = %d", i, j); */
 39 :     pushl %esi                  /* push value of j */
 40 :     pushl %ebx                  /* push value of i */
 41 :     pushl $.LC0                 /* push address of string */
 42 :     call printf
 43 :     addl $12,%esp               /* stack cleanup */
 44 :
 45 :     /* instead of saying j = i * 7, its efficient to say j = j + 7 */
 46 :     addl $7,%esi
 47 :     incl %ebx                   /* i++ */
 48 :     cmpl $9,%ebx                /* is i <= 9, then repeat the loop */
 49 :     jle .L5
 50 :
 51 :     /* return 0; */
 52 :     xorl %eax,%eax
 53 :     leal -8(%ebp),%esp
 54 :     popl %ebx
 55 :     popl %esi
 56 :     leave
 57 :     ret
 58 : .Lfe1:
 59 :     .size    main,.Lfe1-main
 60 :     .ident  "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"
 61 :
 62 : /* end optimized assembly code */
 63 : /* ----------------------------------------------------------------- */
 
Здесь i счетчик цикла , j - induction variable . В оптимизированном asm-коде используются регистры для хранения i и j в EBX в ESI. В 34-й строке EBX(=i) обнуляется,в 35-й аналогично ESI = 0 . Цикл начинается в 37 и продолжается до 49. Переменная в цикле изменяется на 1 . Вместо умножения i на 7 , к j прибавляется 7
Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье