ОПТИМИЗАЦИЯ ТРАССИРОВАНИЯ С
ИСПОЛЬЗОВАНИЕМ EXPRESSION TEMPLATES
Игорь Гусаров
Software Expert, Kaspersky Lab
БУДЕМ ГОВОРИТЬ О FRONT-END
2
СКОЛЬКО СТОИТ ОТЛАДОЧНЫЙ ВЫВОД
КТО ВИНОВАТ И ЧТО ДЕЛАТЬ
РЕЗУЛЬТАТЫ И ПЛАНЫ22
7
3
Ради большей наглядности фрагменты исходного текста C++ приведены на слайдах в упрощённом виде.
Или история про то, как маленькая функция раздулась до десяти
килобайт кода.
СКОЛЬКО СТОИТ ОТЛАДОЧНЫЙ ВЫВОД
PERFORMANCE TEAM
4
Исследует производительность программ и их влияние на
производительность ОС.
- задержки при Startup / Boot / Hibernate / Resume;
- профилирование, поиск узких мест;
- оптимизация основных сценариев;
- контроль потребления ресурсов.
Проблема: рыхлый код
ПРОВЕДЁМ ЭКСПЕРИМЕНТ
5
1. Возьмём реальный продукт - KIS 2015.
2. Вырежем для опытов фрагмент на 3 млн строк.
3. Сколько в нём отладочного вывода? 15 тыс строк.
4. Как изменится размер исполняемых модулей,
если выкинуть весь отладочный вывод?
10%
объёма исполняемых модулей
0,5%
строк исходного текста C++
СИНТЕТИЧЕСКИЙ ПРИМЕР
6
void TestTraceStream()
{
TRACE(level) << "Hello" << ' ' << "World!";
SomeFunc();
}
void TestTracePrintf()
{
TRACE(level, "%s%c%s", "Hello", ' ', "World!");
SomeFunc();
}
подготовка
вывод сообщения
полезная нагрузка
Разберёмся в деталях - откуда взялось так много кода и как сделать
так, чтобы его стало меньше.
КТО ВИНОВАТ И ЧТО ДЕЛАТЬ
#define MY_WARN() TRACE_WARN() << "MyClass(" << this << ")::" FUNCTION " "
#define LOGGED_RETURN(code) do { MY_WARN() << (code); return (code); } while(0)
// Now replace every 'return' with 'LOGGED_RETURN'
void Service::Method()
{
try
{
TRACE_INFO() << "Trying";
DoSomething();
}
catch (...)
{
TRACE_ERR() << "Failed";
}
}
void Service::~Service()
{
TRACE_INFO() << "Done " << m_filename;
}
ЧТО ПИШУТ РАЗРАБОТЧИКИ
8
ЧТО ДОЛЖЕН УМЕТЬ ТРАССИРОВЩИК
9
1. Использовать синтаксис потоков вывода C++.
Поскольку он всем знаком, и давно используется в кодовой базе.
2. Выводить любые типы данных.
И чтобы разработчикам не требовалось писать ничего сложнее привычного оператора << для нового типа.
3. Работать из разных потоков (threads).
Не создавая конкуренции. Простаивать на синхронизации из-за отладочного вывода - это последнее дело.
4. Не создавать лишних исключений.
Мало кому понравится, если отладочный вывод начнёт влиять на ход работы смыслового кода.
5. Не вычислять выводимые значения, если они не нужны.
И вообще тратить минимум усилий на выражения, которые не должны выводиться.
ЧТО СКРЫВАЕТСЯ ЗА МАКРОСАМИ
10
if (!ShouldTrace(GET_TRACER(), LevelWarn))
(void)0;
else
MakeTraceStream(GET_TRACER(), LevelWarn) << ...
if (const TraceHolder& h = TraceHolder(GET_TRACER(), LevelWarn))
(void)0;
else
MakeTraceStream(h.tracer, h.level).SelfRef() << ...MakeTraceStream(h.tracer, h.level).SelfRef() << ...
#define MY_WARN() TRACE_WARN() << "MyClass(" << this << ")::" FUNCTION " "
ЧТО ВЫНУЖДЕН ДЕЛАТЬ КОМПИЛЯТОР
11
MakeTraceStream(tracer, level).SelfRef() << foo() << "data size = " << m_x ;
Не оптимальный по размеру и простоте машинный код
Необходим код
раскрутки стека
Захват критической секции
или создание буфера
Выход: требуется
деструктор
Инлайн-подстановка
операторов вывода
Вход: создание
временного объекта
MakeTraceStream(h.tracer, h.level).SelfRef() << ...
ЧТО БУДЕМ ОПТИМИЗИРОВАТЬ
12
Необходим код
раскрутки стека
Инлайн-подстановка
операторов вывода
Вход: создание
временного объекта3. Избавимся от повторяющегося кода.
Будь то инлайн-подстановка или инстанциация кучи сложных функций.
2. Минимизируем объём кода в точке вызова.
Особенно тех инструкций, которые выполняются при выключенном выводе.
1. Избавимся от кода обработки исключений.
Не пожертвовав при этом ни exception safety, ни thread safety.
MakeTraceStream(tracer, level).SelfRef() << foo() << "data size = " << m_x ;tracer level foo() "data size = " m_xMakeTraceStream SelfRef << << << ;
Надо вычислять на месте
Эти вычисления можно вынести в отдельную общую функцию
КАК БУДЕМ ОПТИМИЗИРОВАТЬ
13
MakeTraceStream(tracer, level).SelfRef() << foo() << "data size = " << m_x ;
TracePut(tracer, { level, foo(), "data size = ", m_x });
tracer <<= KLTRACE_LAZY_OUTPUT() << level << foo() << "data size = " << m_x;
tracer <<= SomeSpecialType() << level << foo() << "data size = " << m_x;
1
что
2
куда
3
как
ЧТО ДАЮТ EXPRESSION TEMPLATES
14
a + b * c
ExprPlus< Ta, ExprMult< Tb, Tc > >
&a, &b, &c
тип :
содержимое :
SomeSpecialType() << level << foo() << "data size = " << m_x;
ArgumentPack<Typelist<> >
ArgumentPack<Typelist<level_t> >
ArgumentPack<Typelist<level_t, long> >
ArgumentPack<Typelist<level_t, long, const char [13]> >
ArgumentPack<Typelist<level_t, long, const char [13], int> > &a1 &a2 &a3 &a4
&a1 &a2 &a3
&a1 &a2
&a1
Они позволяют сформировать кортеж из аргументов, отложив вычисления на потом.
ВОЛШЕБНЫЙ ОПЕРАТОР <<=
15
template <typename TracerType, typename Typelist>
TracerType& operator<<=(TracerType& tracer, const ArgumentPack<Typelist>& args);
Задача: убрать параметризацию функции вывода по Typelist.
Заменим параметризацию типом на параметризацию данными.
ArgumentPack
const T1* p1
const T2* p2
const Tn* pn
...
{
const Descriptor* d = &DescriptorsFor<TracerType, Typelist>::head;
DoOutput<TracerType>(tracer, args.begin(), d);
}
1-2 типа несколько тысяч комбинаций
addr p1; func* f1
addr p2; func* f2
addr pn; func* fn
...
addr p1
addr p2
addr pn
...
+
func* f1
func* f2
func* fn
...
NULL
ВЫВОД ОДНОГО ЗНАЧЕНИЯ
16
WorkerFunc
Функция с неизменным типом аргументов, которая выводит конкретный тип данных в конкретный тип потока.
template <typename TracerType, typename ValueType>
void OutputWorker(void* tracer, addr_t valuePtr)
{
*(TracerType*)tracer << *(const ValueType*)valuePtr;
}
СТАТИЧЕСКАЯ ИНИЦИАЛИЗАЦИЯ СПИСКА
17
struct Descriptor
{
WorkerFunc* worker;
const Descriptor* next;
};
// DescriptorsFor consists of a single static member named 'head'.
template <typename TracerType, typename Typelist>
const Descriptor DescriptorsFor<TracerType, Typelist>::head =
{
WorkerFunc<TracerType, Typelist::Head>,
&DescriptorsFor<TracerType, Typelist::Tail>
};
Descriptor
Статически инициализируемый список из ссылок на рабочие функции.
ВЫВОД ВСЕГО ВЫРАЖЕНИЯ
18
template <typename TracerType>
void DoOutput(TracerType& tracer, const addr_t* args, const Descriptor* d)
{
try
{
output_traits<TracerType>::actual_type actualStream(tracer);
for (int i = 0; d; ++i, d = d->next)
d->worker(&actualStream, args[i]);
}
catch (...)
{
}
}
DoOutput
Единая функция для любых операций вывода.
ПРИЯТНЫЕ ОСОБЕННОСТИ
19
1. Простые типы можно класть в ArgumentPack по значению.
template <typename T> struct PackTraits : PackByRef {};
template <> struct PackTraits<int> : PackByVal {};
2. Макрос можно использовать с любым потоковым выводом.
char buf[128];
buf <<= KLTRACE_LAZY_OUTPUT() << "Hello, " << n << " Worlds!";
3. Метод годится для цепочки любых однородных операторов.
dst <<= KLTRACE_LAZY_FORMAT("Hello, %1 %2!n") % n % m_who;
filename <<= KLTRACE_LAZY_PATH() / diskRoot / m_configPath / "config.xml";
4. Можно получить несколько функций на каждый аргумент.
my_container <<= KLTRACE_LAZY_APPEND() + my_vector + my_list + my_range;
// instantiates begin() and end() for each source.
НЕПРИЯТНЫЕ ОСОБЕННОСТИ
20
2. Возможна неоднозначность при выборе оператора <<.
1. Операторы вывода должны быть видны для ADL.
namespace myproj
{
template <typename AnyStream, typename T1, typename T2>
Stream& operator<<(AnyStream& os, const std::pair<T1, T2>& arg);
void foo(const std::pair<int, int>& data)
{
TRACE_INFO() << data; // Compilation error. Could be solved in C++11
}
} // namespace myproj
template <typename Stream> operator<<(const Stream& os, const MyType& arg);
template <typename X> operator<<(const ArgumentPack& os, const X& arg);
ArgumentPack() << MyType(); // неоднозначность
НЕПРИЯТНЫЕ ОСОБЕННОСТИ
21
3. Нужны специальные меры для вывода std::endl.
template <typename Char, typename Traits>
basic_ostream<Char, Traits>& endl(basic_ostream<Char, Traits>& os);
struct AbstractManipulator; // умеет инициаизироваться выражением std::endl
template <typename Typelist>
operator<<(const ArgumentPack& os, const AbstractManipulator& manip);
4. Нельзя подменять поток в процессе вычисления выражения.
template <typename AnyStream>
CommaInserterStream<AnyStream> operator<<(AnyStream& os, MyCoolManip*);
5. При выключенной оптимизации код становится только хуже.
Для эффективной упаковки аргументов в кортеж обязательно нужна инлайн-подстановка.
Победа!
РЕЗУЛЬТАТЫ
СИНТЕТИЧЕСКИЙ ПРИМЕР
23
void TestTraceStream()
{
TRACE(level) << "Hello" << ' ' << "World!";
SomeFunc();
}
void TestTracePrintf()
{
TRACE(level, "%s%c%s", "Hello", ' ', "World!");
SomeFunc();
}
Stream Printf Expr. Templates
ВОПРОСЫ?
Kaspersky Lab HQ
39A/3 Leningradskoe Shosse
Moscow, 125212, Russian Federation
Tel: +7 (495) 797-8700
www.kaspersky.com
ССЫЛКИ И КОНТАКТЫ
25
"Pimp my log", Marc Eaddy http://www.youtube.com/watch?v=TS_waQZcZVc
Эти слайды доступны на http://meetingcpp.ru/
Пример кода доступен там http://meetingcpp.ru/
Игорь Гусаров Igor.Gusarov@kaspersky.com
Александр Леденев Alexander.Ledenev@kaspersky.com
Андрей Солодовников Andrey.Solodovnikov@kaspersky.com

Оптимизация трассирования с использованием Expression templates

  • 1.
    ОПТИМИЗАЦИЯ ТРАССИРОВАНИЯ С ИСПОЛЬЗОВАНИЕМEXPRESSION TEMPLATES Игорь Гусаров Software Expert, Kaspersky Lab
  • 2.
    БУДЕМ ГОВОРИТЬ ОFRONT-END 2 СКОЛЬКО СТОИТ ОТЛАДОЧНЫЙ ВЫВОД КТО ВИНОВАТ И ЧТО ДЕЛАТЬ РЕЗУЛЬТАТЫ И ПЛАНЫ22 7 3 Ради большей наглядности фрагменты исходного текста C++ приведены на слайдах в упрощённом виде.
  • 3.
    Или история прото, как маленькая функция раздулась до десяти килобайт кода. СКОЛЬКО СТОИТ ОТЛАДОЧНЫЙ ВЫВОД
  • 4.
    PERFORMANCE TEAM 4 Исследует производительностьпрограмм и их влияние на производительность ОС. - задержки при Startup / Boot / Hibernate / Resume; - профилирование, поиск узких мест; - оптимизация основных сценариев; - контроль потребления ресурсов. Проблема: рыхлый код
  • 5.
    ПРОВЕДЁМ ЭКСПЕРИМЕНТ 5 1. Возьмёмреальный продукт - KIS 2015. 2. Вырежем для опытов фрагмент на 3 млн строк. 3. Сколько в нём отладочного вывода? 15 тыс строк. 4. Как изменится размер исполняемых модулей, если выкинуть весь отладочный вывод? 10% объёма исполняемых модулей 0,5% строк исходного текста C++
  • 6.
    СИНТЕТИЧЕСКИЙ ПРИМЕР 6 void TestTraceStream() { TRACE(level)<< "Hello" << ' ' << "World!"; SomeFunc(); } void TestTracePrintf() { TRACE(level, "%s%c%s", "Hello", ' ', "World!"); SomeFunc(); } подготовка вывод сообщения полезная нагрузка
  • 7.
    Разберёмся в деталях- откуда взялось так много кода и как сделать так, чтобы его стало меньше. КТО ВИНОВАТ И ЧТО ДЕЛАТЬ
  • 8.
    #define MY_WARN() TRACE_WARN()<< "MyClass(" << this << ")::" FUNCTION " " #define LOGGED_RETURN(code) do { MY_WARN() << (code); return (code); } while(0) // Now replace every 'return' with 'LOGGED_RETURN' void Service::Method() { try { TRACE_INFO() << "Trying"; DoSomething(); } catch (...) { TRACE_ERR() << "Failed"; } } void Service::~Service() { TRACE_INFO() << "Done " << m_filename; } ЧТО ПИШУТ РАЗРАБОТЧИКИ 8
  • 9.
    ЧТО ДОЛЖЕН УМЕТЬТРАССИРОВЩИК 9 1. Использовать синтаксис потоков вывода C++. Поскольку он всем знаком, и давно используется в кодовой базе. 2. Выводить любые типы данных. И чтобы разработчикам не требовалось писать ничего сложнее привычного оператора << для нового типа. 3. Работать из разных потоков (threads). Не создавая конкуренции. Простаивать на синхронизации из-за отладочного вывода - это последнее дело. 4. Не создавать лишних исключений. Мало кому понравится, если отладочный вывод начнёт влиять на ход работы смыслового кода. 5. Не вычислять выводимые значения, если они не нужны. И вообще тратить минимум усилий на выражения, которые не должны выводиться.
  • 10.
    ЧТО СКРЫВАЕТСЯ ЗАМАКРОСАМИ 10 if (!ShouldTrace(GET_TRACER(), LevelWarn)) (void)0; else MakeTraceStream(GET_TRACER(), LevelWarn) << ... if (const TraceHolder& h = TraceHolder(GET_TRACER(), LevelWarn)) (void)0; else MakeTraceStream(h.tracer, h.level).SelfRef() << ...MakeTraceStream(h.tracer, h.level).SelfRef() << ... #define MY_WARN() TRACE_WARN() << "MyClass(" << this << ")::" FUNCTION " "
  • 11.
    ЧТО ВЫНУЖДЕН ДЕЛАТЬКОМПИЛЯТОР 11 MakeTraceStream(tracer, level).SelfRef() << foo() << "data size = " << m_x ; Не оптимальный по размеру и простоте машинный код Необходим код раскрутки стека Захват критической секции или создание буфера Выход: требуется деструктор Инлайн-подстановка операторов вывода Вход: создание временного объекта MakeTraceStream(h.tracer, h.level).SelfRef() << ...
  • 12.
    ЧТО БУДЕМ ОПТИМИЗИРОВАТЬ 12 Необходимкод раскрутки стека Инлайн-подстановка операторов вывода Вход: создание временного объекта3. Избавимся от повторяющегося кода. Будь то инлайн-подстановка или инстанциация кучи сложных функций. 2. Минимизируем объём кода в точке вызова. Особенно тех инструкций, которые выполняются при выключенном выводе. 1. Избавимся от кода обработки исключений. Не пожертвовав при этом ни exception safety, ни thread safety. MakeTraceStream(tracer, level).SelfRef() << foo() << "data size = " << m_x ;tracer level foo() "data size = " m_xMakeTraceStream SelfRef << << << ; Надо вычислять на месте Эти вычисления можно вынести в отдельную общую функцию
  • 13.
    КАК БУДЕМ ОПТИМИЗИРОВАТЬ 13 MakeTraceStream(tracer,level).SelfRef() << foo() << "data size = " << m_x ; TracePut(tracer, { level, foo(), "data size = ", m_x }); tracer <<= KLTRACE_LAZY_OUTPUT() << level << foo() << "data size = " << m_x; tracer <<= SomeSpecialType() << level << foo() << "data size = " << m_x; 1 что 2 куда 3 как
  • 14.
    ЧТО ДАЮТ EXPRESSIONTEMPLATES 14 a + b * c ExprPlus< Ta, ExprMult< Tb, Tc > > &a, &b, &c тип : содержимое : SomeSpecialType() << level << foo() << "data size = " << m_x; ArgumentPack<Typelist<> > ArgumentPack<Typelist<level_t> > ArgumentPack<Typelist<level_t, long> > ArgumentPack<Typelist<level_t, long, const char [13]> > ArgumentPack<Typelist<level_t, long, const char [13], int> > &a1 &a2 &a3 &a4 &a1 &a2 &a3 &a1 &a2 &a1 Они позволяют сформировать кортеж из аргументов, отложив вычисления на потом.
  • 15.
    ВОЛШЕБНЫЙ ОПЕРАТОР <<= 15 template<typename TracerType, typename Typelist> TracerType& operator<<=(TracerType& tracer, const ArgumentPack<Typelist>& args); Задача: убрать параметризацию функции вывода по Typelist. Заменим параметризацию типом на параметризацию данными. ArgumentPack const T1* p1 const T2* p2 const Tn* pn ... { const Descriptor* d = &DescriptorsFor<TracerType, Typelist>::head; DoOutput<TracerType>(tracer, args.begin(), d); } 1-2 типа несколько тысяч комбинаций addr p1; func* f1 addr p2; func* f2 addr pn; func* fn ... addr p1 addr p2 addr pn ... + func* f1 func* f2 func* fn ... NULL
  • 16.
    ВЫВОД ОДНОГО ЗНАЧЕНИЯ 16 WorkerFunc Функцияс неизменным типом аргументов, которая выводит конкретный тип данных в конкретный тип потока. template <typename TracerType, typename ValueType> void OutputWorker(void* tracer, addr_t valuePtr) { *(TracerType*)tracer << *(const ValueType*)valuePtr; }
  • 17.
    СТАТИЧЕСКАЯ ИНИЦИАЛИЗАЦИЯ СПИСКА 17 structDescriptor { WorkerFunc* worker; const Descriptor* next; }; // DescriptorsFor consists of a single static member named 'head'. template <typename TracerType, typename Typelist> const Descriptor DescriptorsFor<TracerType, Typelist>::head = { WorkerFunc<TracerType, Typelist::Head>, &DescriptorsFor<TracerType, Typelist::Tail> }; Descriptor Статически инициализируемый список из ссылок на рабочие функции.
  • 18.
    ВЫВОД ВСЕГО ВЫРАЖЕНИЯ 18 template<typename TracerType> void DoOutput(TracerType& tracer, const addr_t* args, const Descriptor* d) { try { output_traits<TracerType>::actual_type actualStream(tracer); for (int i = 0; d; ++i, d = d->next) d->worker(&actualStream, args[i]); } catch (...) { } } DoOutput Единая функция для любых операций вывода.
  • 19.
    ПРИЯТНЫЕ ОСОБЕННОСТИ 19 1. Простыетипы можно класть в ArgumentPack по значению. template <typename T> struct PackTraits : PackByRef {}; template <> struct PackTraits<int> : PackByVal {}; 2. Макрос можно использовать с любым потоковым выводом. char buf[128]; buf <<= KLTRACE_LAZY_OUTPUT() << "Hello, " << n << " Worlds!"; 3. Метод годится для цепочки любых однородных операторов. dst <<= KLTRACE_LAZY_FORMAT("Hello, %1 %2!n") % n % m_who; filename <<= KLTRACE_LAZY_PATH() / diskRoot / m_configPath / "config.xml"; 4. Можно получить несколько функций на каждый аргумент. my_container <<= KLTRACE_LAZY_APPEND() + my_vector + my_list + my_range; // instantiates begin() and end() for each source.
  • 20.
    НЕПРИЯТНЫЕ ОСОБЕННОСТИ 20 2. Возможнанеоднозначность при выборе оператора <<. 1. Операторы вывода должны быть видны для ADL. namespace myproj { template <typename AnyStream, typename T1, typename T2> Stream& operator<<(AnyStream& os, const std::pair<T1, T2>& arg); void foo(const std::pair<int, int>& data) { TRACE_INFO() << data; // Compilation error. Could be solved in C++11 } } // namespace myproj template <typename Stream> operator<<(const Stream& os, const MyType& arg); template <typename X> operator<<(const ArgumentPack& os, const X& arg); ArgumentPack() << MyType(); // неоднозначность
  • 21.
    НЕПРИЯТНЫЕ ОСОБЕННОСТИ 21 3. Нужныспециальные меры для вывода std::endl. template <typename Char, typename Traits> basic_ostream<Char, Traits>& endl(basic_ostream<Char, Traits>& os); struct AbstractManipulator; // умеет инициаизироваться выражением std::endl template <typename Typelist> operator<<(const ArgumentPack& os, const AbstractManipulator& manip); 4. Нельзя подменять поток в процессе вычисления выражения. template <typename AnyStream> CommaInserterStream<AnyStream> operator<<(AnyStream& os, MyCoolManip*); 5. При выключенной оптимизации код становится только хуже. Для эффективной упаковки аргументов в кортеж обязательно нужна инлайн-подстановка.
  • 22.
  • 23.
    СИНТЕТИЧЕСКИЙ ПРИМЕР 23 void TestTraceStream() { TRACE(level)<< "Hello" << ' ' << "World!"; SomeFunc(); } void TestTracePrintf() { TRACE(level, "%s%c%s", "Hello", ' ', "World!"); SomeFunc(); } Stream Printf Expr. Templates
  • 24.
    ВОПРОСЫ? Kaspersky Lab HQ 39A/3Leningradskoe Shosse Moscow, 125212, Russian Federation Tel: +7 (495) 797-8700 www.kaspersky.com
  • 25.
    ССЫЛКИ И КОНТАКТЫ 25 "Pimpmy log", Marc Eaddy http://www.youtube.com/watch?v=TS_waQZcZVc Эти слайды доступны на http://meetingcpp.ru/ Пример кода доступен там http://meetingcpp.ru/ Игорь Гусаров Igor.Gusarov@kaspersky.com Александр Леденев Alexander.Ledenev@kaspersky.com Андрей Солодовников Andrey.Solodovnikov@kaspersky.com

Editor's Notes

  • #2 To add background use Background button and choose from drop-down menu. Please mind the size of text areas.
  • #3 To update contents automatically fill in names of dividing slides and press Update button. Please mind the size of text areas.
  • #4 To add Dividing slide use Add section, or select from Structure drop down list. When a new Dividing slide is inserted, Content slide will be automatically updated. Please mind the size of text areas.
  • #5 Please mind the size of text areas.
  • #6 Please mind the size of text areas.
  • #7 Please mind the size of text areas.
  • #8 To add Dividing slide use Add section, or select from Structure drop down list. When a new Dividing slide is inserted, Content slide will be automatically updated. Please mind the size of text areas.
  • #9 Please mind the size of text areas.
  • #10 Please mind the size of text areas.
  • #11 Please mind the size of text areas.
  • #12 Please mind the size of text areas.
  • #13 Please mind the size of text areas.
  • #14 Please mind the size of text areas.
  • #15 Please mind the size of text areas.
  • #16 Please mind the size of text areas.
  • #17 Please mind the size of text areas.
  • #18 Please mind the size of text areas.
  • #19 Please mind the size of text areas.
  • #20 Please mind the size of text areas.
  • #21 Please mind the size of text areas.
  • #22 Please mind the size of text areas.
  • #23 To add Dividing slide use Add section, or select from Structure drop down list. When a new Dividing slide is inserted, Content slide will be automatically updated. Please mind the size of text areas.
  • #24 Please mind the size of text areas.
  • #25 The best way to finish your presentation is to place call to action text or contacts. Try to initiate feedback and further communication. Please mind the size of text areas.
  • #26 Please mind the size of text areas.