Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: [C++] как лучше и быстрее (switch или указатель на фукцию) ?  (Прочитано 23183 раз)
0 Пользователей и 2 Гостей смотрят эту тему.
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« : 16-12-2009 11:56 » 

Привет!

Есть приложение, 99% времени свой работы оно выполняет некий набор инструкций более 100 раз в секунду - такие требования к скорости.
Это реализовано так:
есть главный цикл, внутри которого этот самый набор инструкций - и этот цикл должен выполнятЬся не менее 100 раз в секунду.

Наборы инструкций разные в зависимости от состояния определяющей переменной.
В определенные моменты нужно переключать этот самый набор - например, сейчас приложение выполняет набор команд А, потом переключается на выполнение команд B - и т.д.

Вопрос - как лучше это переключение организовать - через switch и case или через указатель на фунцию?

Допустим:
Код:
enum {A, B, C} State;
while(true)  //главный цикл приложения
{
      switch(State)
      {
            case A: /*Инструкции А*/ break;
            case B: /*Инструкции B*/ break;
            case C: /*Инструкции C*/ break;
            default: break;
      }
}

Таким образом, работа приложения определяется значением перечислимой константой Sate. Цикл всегда выполяняется 100 раз в секунду - но что именно в нем выполяется - зависит от State.

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

Как грамотнее и быстрее? Или, может, вообще другой вариант?
Спасибо:)

Правка: грамотный выход из цикла, естественно, есть - в примере я этого не отразил.
Правка 2: case'ов может быть около 10 - не больше.
« Последнее редактирование: 16-12-2009 13:21 от Sel » Записан
Sla
Модератор

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #1 : 16-12-2009 12:27 » 

Реализация зависит от платформы.
Как по мне, так вариант с CASE более красив и удобен для последующего сопровождения кода.



Если "развернуть" в асм код CASE, можно увидеть кучу условных переходов или же "табличную" адресацию"?
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
Вад
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 16-12-2009 12:29 » 

Имхо, switch очевиднее и проще: если потеря в производительности не играет большой роли, и case-ов практически 100% не станет со временем на порядки больше, - то я сделал бы через switch.

С другой стороны, если заведомо число вариантов неизвестно, или этот код вызывается сотни тысяч-миллионы раз в секунду, то указатели на функции будут предпочтительнее/быстрее. Хотя насчёт быстрее я теперь уже не уверен Улыбаюсь
Другой случай, когда указатели более удобны, чем свитч, - это когда, скажем, нужно сделать фабрику для развивающейся иерархии типов. То есть, чтобы при добавлении нового типа не требовалось заносить изменения в свитч и плодить зависимости между фабрикой и порождаемыми ею типами.
« Последнее редактирование: 16-12-2009 12:32 от Вад » Записан
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #3 : 16-12-2009 19:25 » 

Sla,  
Цитата
Если "развернуть" в асм код CASE, можно увидеть кучу условных переходов или же "табличную" адресацию"?
Я вот не знаю - подскажите - как это увидеть? Как посмотреть на асм-код? Пользуюсь Студией.
Цитата
Реализация зависит от платформы.
Windows, + возможно портирование на MacOS.

Вад, пример с фабрикой иерархии типов - что-то близкое:) Только у меня будут добавляться новые наборы команд - т.е. действительно придется вносить изменение и в enum и в switch - каждый раз добавлять.

Вопрос быстродействия, похоже, не имеет очевидного ответа. Но case'ов не будет больше 10-ти - 100%.  Что же выбрать)
Чем будет менее затратно по производительности - тем лучше.
Записан
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #4 : 16-12-2009 20:24 » 

За сколько времени у тебя выполняется набор команд? И это время соизмеримо со временем на переключение в другую систему команд?

В принципе тут можно сделать С++ way. Создать родительский базовый класс. С виртуальной функцией run. Для своего набора команд, наследоваться от родительского класса. Теперь только останется подменять классы. При этом исключаются всякие хаки.
« Последнее редактирование: 16-12-2009 20:26 от Finch » Записан

Не будите спашяго дракона.
             Джаффар (Коша)
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 16-12-2009 22:18 » 

Finch,
Цитата
За сколько времени у тебя выполняется набор команд? И это время соизмеримо со временем на переключение в другую систему команд?
Честно говоря - не измерял, за сколько времени выполняется один набор команд - но точно могу сказать, что это время НЕ соизмеримо со временем, затрачиваемым на переключение в другую систему команд - последнее происходит существенно быстрее)

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

Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #6 : 17-12-2009 06:44 » 

Так проверь, чего спрашивать7 Покрути 2 цикла с одинаково большим количеством итераций и определи, который выполняется дольше. Тебе ж про switch тоже говорили, что там череда сравнений выполняется. 1 или 2 раза взять содержимое по указателю или выполнить 1-10 сравнений?
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #7 : 17-12-2009 09:12 » 

Dimka, просто все еще на стадии проектирования в основном - крутить нечего. Пока только один набор операций. 
Но так и сделаю - раз по-другому не понять:) Хотя думаю, что с указателем будет быстрее.

Спасибо всем за ответы!
Записан
Sla
Модератор

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #8 : 17-12-2009 09:31 » 

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

Про платформу я говорил, понимая не ОС, а аппаратную часть.
Если это МК, то оптимизация имеет смысл. Но когда речь идет об С++, то понятно, что об МК и речи быть не может.
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #9 : 17-12-2009 09:37 » 

Цитата
. Тебе ж про switch тоже говорили, что там череда сравнений выполняется. 1 или 2 раза взять содержимое по указателю или выполнить 1-10 сравнений?

странно, всегда полагал, что там "табличный" переход Улыбаюсь

Код:
	int i=3;
switch(i)
{
case 0:
{
int iii0=0;
}
break;

case 1:
{
int iii1=0;
}
break;

case 2:
{
int iii2=0;
}
break;

case 3:
{
int iii3=0;
}
break;
}

int iii=1;

alt+8
Код:
	int i=3;
005AD4A2  mov         dword ptr [ebp-20h],3
switch(i)
005AD4A9  mov         eax,dword ptr [ebp-20h]
005AD4AC  mov         dword ptr [ebp-174h],eax
005AD4B2  cmp         dword ptr [ebp-174h],3
005AD4B9  ja          $LN2+7 (5AD4EAh)
005AD4BB  mov         ecx,dword ptr [ebp-174h]
005AD4C1  jmp         dword ptr  (5AD8F4h)[ecx*4]
{
case 0:
{
int iii0=0;
005AD4C8  mov         dword ptr [iii0],0
}
break;
005AD4CF  jmp         $LN2+7 (5AD4EAh)

case 1:
{
int iii1=0;
005AD4D1  mov         dword ptr [iii1],0
}
break;
005AD4D8  jmp         $LN2+7 (5AD4EAh)

case 2:
{
int iii2=0;
005AD4DA  mov         dword ptr [iii2],0
}
break;
005AD4E1  jmp         $LN2+7 (5AD4EAh)

case 3:
{
int iii3=0;
005AD4E3  mov         dword ptr [iii3],0
}
break;
}




int iii=1;
005AD4EA  mov         dword ptr [ebp-5Ch],1


вычисление адреса перехода - один раз:
Код:
005AD4BB  mov         ecx,dword ptr [ebp-174h] 
005AD4C1  jmp         dword ptr  (5AD8F4h)[ecx*4]

а после каждого блока - выход через одну точку
Код:
005AD4CF  jmp         $LN2+7 (5AD4EAh) 

Записан

The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #10 : 17-12-2009 09:40 » 

Sla, понимаю, просто целевые компьютеры начинаются от компьютеров 5-7 летней давности в том числе - надо, чтобы и на них все работало прекрасно.
Улыбаюсь
И просто я слабо еще представляю - где будут большие затраты, а где маленькие, поэтому для меня не очевидно, что я пытаюсь "ловить блох на мизерной оптимизации", пока вы мне не подскажите:)
В книжках написано, что виртуальные функции работают медленнее невиртуальных из-за позднего связывания.
Записан
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #11 : 17-12-2009 09:41 » 

The Nameless One, быстрее свича ничего не сделаешь Улыбаюсь
Записан

The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #12 : 17-12-2009 09:46 » 

Алексей1153++, спасибо Улыбаюсь
Записан
Sla
Модератор

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #13 : 17-12-2009 09:49 » 

Алексей1153++,  ты бы подсказал человеку, как ты получил asm код. А он спрашивал (а я не знаю).
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #14 : 17-12-2009 09:52 » 

Sla,  если не ошибаюсь, то там в посту у Алексей1153++ перед asm кодом чтоит alt+8 - наверное, это есть оно? Улыбаюсь
Записан
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #15 : 17-12-2009 09:55 » 

туда можно попасть только в запущенном на дебаг коде - ставим курсор на интерисующее место и жмём alt+8 (по дефолтной настройке в стиле VS6) Улыбаюсь
Записан

The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #16 : 17-12-2009 09:56 » 

Ясно:)
Записан
Sla
Модератор

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #17 : 17-12-2009 10:07 » 

Алексей1153++, "табличный переход" это частный случай условного перехода.

А теперь представь не строготипизированный язык и попытайся развернуть switch. Или же  отсутствие
конструкций типа dword ptr
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #18 : 17-12-2009 10:13 » 

Слав, в ТЗ такого не было Улыбаюсь
Записан

Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #19 : 17-12-2009 14:55 » 

Алексей1153++, не понял насчёт таблицы.

Берём VS 2008 C++ native без оптимизации

Код:
#include <iostream>

int main()
{
int x;
std::cin >> x;
switch(x)
{
case 0:
std::cout << "A";
break;
case 1:
std::cout << "B";
break;
case 2:
std::cout << "C";
break;
default:
std::cout << "Z";
break;
}
return 0;
}

Смотрим дизассемблер:
Код:
#include <iostream>

int main()
{
00B014A0  push        ebp 
00B014A1  mov         ebp,esp
00B014A3  sub         esp,0D0h
00B014A9  push        ebx 
00B014AA  push        esi 
00B014AB  push        edi 
00B014AC  lea         edi,[ebp-0D0h]
00B014B2  mov         ecx,34h
00B014B7  mov         eax,0CCCCCCCCh
00B014BC  rep stos    dword ptr es:[edi]
int x;
std::cin >> x;
00B014BE  mov         esi,esp
00B014C0  lea         eax,[x]
00B014C3  push        eax 
00B014C4  mov         ecx,dword ptr [__imp_std::cin (0B0A338h)]
00B014CA  call        dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::operator>> (0B0A33Ch)]
00B014D0  cmp         esi,esp
00B014D2  call        @ILT+405(__RTC_CheckEsp) (0B0119Ah)
switch(x)
00B014D7  mov         eax,dword ptr [x]
00B014DA  mov         dword ptr [ebp-0D0h],eax
00B014E0  cmp         dword ptr [ebp-0D0h],0
00B014E7  je          main+5Dh (0B014FDh)
00B014E9  cmp         dword ptr [ebp-0D0h],1
00B014F0  je          main+72h (0B01512h)
00B014F2  cmp         dword ptr [ebp-0D0h],2
00B014F9  je          main+87h (0B01527h)
00B014FB  jmp         main+9Ch (0B0153Ch)
{
case 0:
std::cout << "A";
00B014FD  push        offset string "A" (0B0780Ch)
00B01502  mov         eax,dword ptr [__imp_std::cout (0B0A320h)]
00B01507  push        eax 
00B01508  call        std::operator<<<std::char_traits<char> > (0B0114Fh)
00B0150D  add         esp,8
break;
00B01510  jmp         main+0AFh (0B0154Fh)
case 1:
std::cout << "B";
00B01512  push        offset string "B" (0B07808h)
00B01517  mov         eax,dword ptr [__imp_std::cout (0B0A320h)]
00B0151C  push        eax 
00B0151D  call        std::operator<<<std::char_traits<char> > (0B0114Fh)
00B01522  add         esp,8
break;
00B01525  jmp         main+0AFh (0B0154Fh)
case 2:
std::cout << "C";
00B01527  push        offset string "C" (0B07804h)
00B0152C  mov         eax,dword ptr [__imp_std::cout (0B0A320h)]
00B01531  push        eax 
00B01532  call        std::operator<<<std::char_traits<char> > (0B0114Fh)
00B01537  add         esp,8
break;
00B0153A  jmp         main+0AFh (0B0154Fh)
default:
std::cout << "Z";
00B0153C  push        offset string "Z" (0B07800h)
00B01541  mov         eax,dword ptr [__imp_std::cout (0B0A320h)]
00B01546  push        eax 
00B01547  call        std::operator<<<std::char_traits<char> > (0B0114Fh)
00B0154C  add         esp,8
break;
}
return 0;
00B0154F  xor         eax,eax
}
00B01551  push        edx 
00B01552  mov         ecx,ebp
00B01554  push        eax 
00B01555  lea         edx,[ (0B01578h)]
00B0155B  call        @ILT+160(@_RTC_CheckStackVars@8) (0B010A5h)
00B01560  pop         eax 
00B01561  pop         edx 
00B01562  pop         edi 
00B01563  pop         esi 
00B01564  pop         ebx 
00B01565  add         esp,0D0h
00B0156B  cmp         ebp,esp
00B0156D  call        @ILT+405(__RTC_CheckEsp) (0B0119Ah)
00B01572  mov         esp,ebp
00B01574  pop         ebp 
00B01575  ret             
00B01576  mov         edi,edi
00B01578  db          01h 
00B01579  db          00h 
00B0157A  db          00h 
00B0157B  db          00h 
00B0157C  db          80h 
00B0157D  db          15h 
00B0157E  db          b0h 
00B0157F  db          00h 
00B01580  db          f8h 
00B01581  db          ffh 
00B01582  db          ffh 
00B01583  db          ffh 
00B01584  db          04h 
00B01585  db          00h 
00B01586  db          00h 
00B01587  db          00h 
00B01588  db          8ch 
00B01589  db          15h 
00B0158A  db          b0h 
00B0158B  db          00h 
00B0158C  db          78h 
00B0158D  db          00h 

Имеем на switch цепочку из cmp-je операторов.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #20 : 17-12-2009 19:56 » 

А вот сделать абстрактный базовый класс с виртуальным методом и от него наследовать классы, в методы которых "рассовать" наборы команд, а в главном цикле просто "крутить" указатель на базовый класс с вызовом метода - думал над этим. Только с быстродействием вопрос - позднее связывание - сначала надо определить - на какой именно класс смотрит указатель - потом надо вызвать соответствующую функцию - так это вроде работает?   А ведь в ней только самое "мясо" и будет - со сложными расчетами и вложенными вызовами функций. Не будет ли это затратней, чем case?
Ты слишком усложняеш. Вот простенкий пример программы. Которая показывает мою мысль.
Код:
include <stdio.h>
class absAction
{
 public:
  absAction() {};
  virtual ~absAction() {};
  virtual void run() {}; 
};

class firstAction :public absAction
{
 public:
  firstAction() : absAction() {};
  virtual ~firstAction() {};
  virtual void run() {
  //some action
  printf("First action class\n");
  };
};

class secondAction :public absAction
{
 public:
  secondAction() : absAction() {};
  virtual ~secondAction() {};
  virtual void run() {
  //some action
  printf("Second action class\n");
  };
};

class somePointer {
 public:
  somePointer() {
  action=NULL;
     setAction(new absAction());
  }
 
  somePointer(absAction *val) {
  action=NULL;
     if (!setAction(val)) setAction(new absAction());
  }
 
  bool setAction(absAction *val) {
  bool res=false;
  if (val != NULL) {
  delete action;
  action = val;
  res=true;
  }
  return res;
  };
 
  ~somePointer() {delete action;};
 
  void run() {action->run();};
 private:
  absAction *action;
};

int main() {
somePointer ptr(new firstAction());
ptr.run();
ptr.setAction(new secondAction());
ptr.run();
return 0;
}
Записан

Не будите спашяго дракона.
             Джаффар (Коша)
The Nameless One
Помогающий

ru
Offline Offline
Пол: Мужской

« Ответ #21 : 17-12-2009 21:16 » 

Finch, благодарю за пояснение)
Записан
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #22 : 18-12-2009 04:07 » 

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

upd
Dimka, предположение из области фантастики: у меня все кейсы всегда блоками {} отделены , попробуй, поставь - может, это влияет ?
« Последнее редактирование: 18-12-2009 04:09 от Алексей1153++ » Записан

Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #23 : 18-12-2009 04:18 » new

Dimka, вот и ответ: если добавить кейсов побольше, то случается "табличный" переход Улыбаюсь Твой код у меня так же построился, как и у тебя, а вот такой
Код:
#include <iostream>

int main()
{
int x;
std::cin >> x;
switch(x)
{
case 0:
std::cout << "A";
break;
case 1:
std::cout << "B";
break;
case 2:
std::cout << "C";
break;
case 3:
std::cout << "C";
break;
case 4:
std::cout << "C";
break;
case 5:
std::cout << "C";
break;
case 6:
std::cout << "C";
break;
default:
std::cout << "Z";
break;
}
return 0;
}

уже так
Код:
...
...
5:        int x;
6:        std::cin >> x;
004016A8   lea         eax,[ebp-4]
004016AB   push        eax
004016AC   mov         ecx,offset std::cin (00478888)
004016B1   call        @ILT+580(std::basic_istream<char,std::char_traits<char> >::operator>>) (00401249)
7:        switch(x)
8:        {
004016B6   mov         ecx,dword ptr [ebp-4]
004016B9   mov         dword ptr [ebp-8],ecx
004016BC   cmp         dword ptr [ebp-8],6
004016C0   ja          $L7578+14h (0040175f)
004016C6   mov         edx,dword ptr [ebp-8]
004016C9   jmp         dword ptr [edx*4+401784h]
9:        case 0:
10:           std::cout << "A";
004016D0   push        offset string "A" (0046d028)
004016D5   push        offset std::cout (004787f8)
004016DA   call        @ILT+770(std::operator<<) (00401307)
004016DF   add         esp,8
11:           break;
004016E2   jmp         $L7578+26h (00401771)
12:       case 1:
13:           std::cout << "B";
...
...


полагаю, что компилятор прикидывает, что будет быстрее - три джампа быстрее, чем рассчёт перехода, а 4 джампа, скажем, уже медленнее
« Последнее редактирование: 18-12-2009 04:55 от Алексей1153++ » Записан

Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #24 : 18-12-2009 08:25 » 

а я бы сделал так
Код:
.........
enum EStates { eStateA = 0, eStateB, eStateC, .............., eStateCount}
.........

FunctionType state_handlers[eStateCount] = {0}; // вместо указетелей можно использовать функторы
                                // вместо массива вектор, если нужно динамически добавлять обработчики
...............
while(true)  //главный цикл приложения
{
      state_handlers[State](params);
}
................

Записан

Странно всё это....
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #25 : 18-12-2009 08:27 » 

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

Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #26 : 18-12-2009 09:08 » 

Алексей Улыбаюсь ну ты же знаешь, что я отвечу Улыбаюсь

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

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

Записан

Странно всё это....
Алексей++
глобальный и пушистый
Глобальный модератор

ru
Offline Offline
Сообщений: 13


« Ответ #27 : 18-12-2009 09:19 » 

если код тяжёлый - то да. А если код сам на сотню тактов ? )
Ну об этом можно и не спорить - автор сам решит, что ему "быстрее", а что удобнее ))
Записан

Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines