PHP 7.4刚刚被宣布是稳定的,我们已经提交了更多的改进。 最重要的是,PHP等待着什么可以告诉Dmitry Stogov-开放源码PHP的领先开发者之一,并且可能是最老的活跃贡献者。
德米特里(Dmitry)的所有报告仅涉及他亲自研究的那些技术和解决方案。 在Ontiko的最佳传统中,从PHP 8的Dmitry创新角度出发,最有趣的
故事是文本版本,可以打开新的用例。 首先,准时制和外国直接投资-并非处于“令人惊叹的前景”的关键,而是具有实施细节和陷阱。
供参考: Dmitry Stogov于1984年熟悉编程,那时并非所有读者都出生了,并设法为开发工具(尤其是PHP)的开发做出了重大贡献(尽管Dmitry改进了PHP性能,但并不专门针对俄罗斯开发人员,他们
表示我以HighLoad ++ Award的形式表示感谢)。 Dmitry是Turck MMCache for PHP(eAccelerator)的作者,Zend OPcache维护者,PHPNG项目的负责人,该项目构成了PHP 7的基础,并且是PHP JIT开发的负责人。
PHP性能开发
15年前,我加入Zend时就开始从事PHP性能方面的工作。 然后,我们发布了版本5.0,这是该语言真正成为面向对象的第一个版本。 从那时起,我们已经能够将综合测试的生产率提高40倍,将实际应用的生产率提高6倍。

在这段时间里,有两个突破时刻:
- 5.1版,在其中可以显着提高解释速度。 我们实施了专业的口译员,这主要影响了综合测试。
- 版本7.0,其中处理了所有关键数据结构,从而优化了内存和处理器缓存的工作( 在此处了解有关这些优化的更多信息)。 在合成测试和实际应用中,这导致了两倍以上的加速。
所有其他版本均通过实施许多效果较差的想法逐渐提高了生产率。 例如,在7.1版中,人们非常关注优化字节码(有关这些解决方案
的文章 )。
该图显示,在第5版的开发结束时和第7版的开发周期结束时,我们都进入了平稳状态并放慢了速度。 因此,在去年的v7.4工作中,生产率仅提高了2%。 这还不错,因为出现了诸如类型化属性和协变量类型之类的新功能,这些功能减慢了PHP的速度(Nikita Popov在PHP Russia中
谈到了这些新产品)。
现在,每个人都想知道第八版会带来什么,它能否延续v7的成功?
准时或不准时
改进口译员的想法尚未穷尽,但是所有这些都需要进行大量的研究。 其中的许多必须在概念证明阶段被拒绝,因为所获得的收益证明与复杂性或施加的技术限制无法相比。
但是,仍然有希望获得新的突破性技术-当然,我还记得JIT和JavaScript引擎的成功故事。
实际上,自2012年以来就一直在进行PHP的JIT研究。 我们曾与Intel同事,JavaScript黑客合作过3或4种实现方式,但是以某种方式无法将JIT包含在主分支中。 最后,在PHP 8中,我们将JIT包含在编译器中,并且看到了双重加速,但仅在综合测试上,而在实际应用程序上却出现了放慢速度。

当然,这不是我们要争取的。
怎么了 也许我们做错了事,也许WordPress太糟糕了,没有JIT可以帮助他(是的,实际上是这样)。 也许我们已经使解释器太好了,但是在JavaScript中,情况更糟。 在计算测试中,这是事实:
PHP解释器是最好的之一 。

在Mandelbrot测试中,他甚至超越了以汇编语言编写的解释器LuaJIT之类的宝石。 在此测试中,我们仅比优化的GCC-5.3编译器落后4倍。 使用JIT,我们可以在Mandelbrot测试中获得更好的结果。 实际上,我们已经做到了,也就是说,我们能够生成与C编译器竞争的代码。
那为什么我们不能加速实际应用呢? 为了理解,我将告诉您我们如何进行准时生产。 让我们从基础开始。
PHP如何工作
服务器接受该请求,将其编译为字节码,然后将其发送到虚拟机以执行。 通过执行字节码,虚拟机还可以调用其他PHP文件,这些文件再次重新编译为字节码并再次执行。
查询完成后,将从内存中删除与其相关的所有信息,包括字节码。 也就是说,每个PHP脚本都必须针对每个请求再次进行编译。 当然,将JIT编译嵌入这样的方案根本是不可能的,因为编译器必须非常快。
但是很可能没有人使用裸露的PHP,每个人都将其与OPcache一起使用。
PHP + OPcache
OPcache的主要目标是摆脱对每个请求的重新编译脚本。 它被嵌入专门为此设计的点中,拦截所有编译请求并将编译后的字节码缓存在共享内存中。
同时,不仅节省了编译时间,还节省了内存,因为较早的字节码内存分配在每个进程的地址空间中,现在它存在于单个副本中。
您已经可以在此电路中嵌入JIT,我们将这样做。 但首先,我将向您展示口译员的工作方式。

解释器首先是为每个指令调用其自己的处理程序的循环。
我们使用两个寄存器:
- execute_data-指向当前激活帧的指针;
- opline-指向当前可执行虚拟指令的指针。
使用gcc扩展,这两种类型的寄存器映射到实际的硬件寄存器,因此它们可以非常快速地工作。
在循环中,我们只需为每个指令调用处理程序,然后在每个处理程序的结尾将指针移至下一条指令。
重要的是要注意,处理程序的地址直接写入字节码。 一条指令可以有多个不同的处理程序。 最初是为了专门化而发明的,以便处理程序可以专门研究操作数类型。 JIT使用相同的技术,因为如果您将地址作为处理程序写入新生成的代码中,则将启动JIT处理程序,而无需对解释程序进行任何更改。
在上面的示例中,为加法指令编写的处理程序显示在右侧。 它接受操作数(这里的第一个和第二个可以是常量,临时或局部变量),读取操作数,检查类型,产生直接逻辑-加法-然后返回到循环,将控制权转移到下一个处理程序。
从此描述中生成了专用功能。 由于存在三个可能的第一个操作数,三个可能的第二个操作数,我们得到9个不同的函数。

在这些函数中,代替用于获取操作数的通用方法,而是使用不进行任何检查的特定方法。
混合虚拟机
我们在7.2版中所做的另一个复杂功能是所谓的混合虚拟机。
如果以前我们总是在解释器循环中直接使用间接调用来调用处理程序,那么现在对于每个处理程序,我们都在循环体中另外输入了一个标签,我们可以使用间接跳转跳转到该标签,并直接调用处理程序本身。

似乎他们早先进行了一次间接调用,现在进行了两次:间接转换和直接调用,这样的系统应该运行得更慢。 但是实际上它运行得更快,因为我们可以帮助处理器预测过渡。 以前,只有一个地方可以过渡到其他地方。 处理器经常被误认为是因为它根本不记得有必要先跳转到一条指令,然后再跳转到另一条指令。 现在,在每次直接调用之后,都将间接转换到下一个标签。 结果,当执行PHP循环时,虚拟PHP指令以稳定的顺序排列,然后几乎以线性方式执行。
混合虚拟机又使生产率提高了5-10%。
PHP + OPcache + JIT
JIT作为OPcache的一部分实现。

编译并优化字节码后,将为其启动JIT编译器,该JIT编译器不再与源代码一起使用。 JIT编译器从PHP字节码生成本机代码,然后在字节码中更改第一条指令(实际上是函数)的地址。
之后,无需任何更改就可以从现有解释器中调用已经生成的本机代码。 我将向您展示一个简单的示例。

左侧是用PHP编写的某个函数,该函数对从0到100的数字之和进行计数。在右侧,是生成的字节码。 第一条指令为总和分配0,第二条指令对i分配相同,然后无条件跳转到标签。 在标签L1上,检查退出循环的条件:如果满足,则退出,如果不满足,则进入循环。 接下来,将i加到总和中,将结果写成数量,然后将i加1。
直接从这里生成汇编代码,结果非常好。

第一条
QM_ASSIGN
指令
QM_ASSIGN
编译为两条机器指令(2-3行)。
%esi
寄存器包含一个指向当前激活帧的指针。 在偏移量30处存在可变量。 第一条指令写入值0,第二条指令写入值4-这是整数类型(
IS_LONG
)的标识符。 对于变量
i
编译器意识到它总是很长,因此不需要存储类型。 此外,它可以存储在机器寄存器中。 因此,这里简单地将寄存器与自身进行XOR是最简单,最便宜的复位指令。
然后,以相同的方式进行无条件转换,我们检查是否发生了某些外部事件,检查周期的条件,然后进入周期。 在循环中,检查总和是否为整数:如果是,则读取整数值,将值i加上它,检查是否有溢出,将结果写回总和并将1加到
%edx
。
可以看出,该代码已接近最优。 甚至可以优化它,而不必在循环的每次迭代中检查类型的总和。 但这已经是相当复杂的优化,我们还没有这样做。
我们正在将JIT开发为一种相当简单的技术 ,我们并未尝试执行Java HotSpot正在尝试做的V8,我们的功能更少。
jit怎么了
为什么使用如此好的汇编代码,我们不能加速实际的应用程序?
其实应该吗?
- 如果瓶颈不在CPU中,那么JIT将无济于事。
- 生成太多代码(代码膨胀)。
- 静态类型推断并非始终有效。
- 诚实的代码(用于从未执行过的情况)。
- 支持虚拟机的一致状态(突然出现异常)。
- 上课仅适用于一个请求。
如果应用程序在80%的时间内等待数据库的响应,则JIT将无济于事。 如果我们调用外部资源密集型函数(例如,与正则表达式匹配),那么JIT也将以相同的方式调用相同的函数。 此外,如果应用程序先构建大型数据结构-树,图,然后再读取它们,那么在JIT的帮助下,我们生成的代码将读取较少的指令,但要加载数据本身,则需要花费全部时间。您还需要加载代码。
正如您已经看到的那样,JIT甚至可以减慢实际应用程序的速度,因为它会生成大量代码,并且读取它会成为问题-当读取大量代码时,其他数据被迫移出缓存,从而导致速度降低。
PHP 8的适度计划
我们要在PHP 8中实现的改进之一是
生成更少的代码 。 现在,正如我所说,我们为整个脚本生成本机代码,并在加载阶段加载。 但是肯定不会调用其中一半功能。 因此,我们进行了进一步介绍,并引入了一个触发器,该触发器使我们可以配置何时运行JIT。 可以运行:
- 用于所有功能;
- 仅适用于首次调用的函数;
- 您可以在每个函数上挂一个计数器,并仅编译真正热的那些函数。
这样的方案可能会更好一些,但仍然不是最佳方案,因为在每个函数中同样存在被执行的路径和从未执行过的路径。 由于PHP是一种动态编程语言,也就是说,每个变量可以具有不同的类型,因此您需要支持静态分析器预测的所有类型。 当他无法证明其他类型的人不能做到这一点时,他经常会谨慎地做。
在这种情况下,我们将摆脱诚实的编译,开始进行推测性的编译。
将来,我们计划首先在应用程序工作期间分析“最热”的函数,查看程序的路径,变量的类型,甚至还记得边界条件,然后才生成针对当前最优的函数代码执行方式-仅适用于那些实际执行的部分。
对于其他所有内容,我们将放置存根。 一样,将存在检查和可能的输出,从该处开始去优化过程,也就是说,我们将还原解释所需的虚拟机状态,并将其交给解释器执行。
HotSpot Java VM和V8中都使用了类似的方案。 但是使技术适应PHP存在许多困难。 首先,这是我们共享了来自不同进程的字节码和本地代码。 我们不能直接在共享内存中更改它们,我们必须首先复制某个地方,进行更改,然后再提交回共享内存。
预加载。 类绑定问题
实际上,PHP 7甚至PHP 5中长期包含的许多PHP增强思想都来自JIT相关工作。 今天,我将讨论另一个这样的技术-这是预加载。 该技术已包含在PHP 7.4中,它使得可以指定一组文件,在服务器启动时加载它们,并使这些文件的所有功能永久化。
预加载技术解决的问题之一是类绑定问题。 事实是,当我们仅用PHP编译文件时,每个文件都是独立编译的。 这样做是因为它们每个都可以分别更改。 您不能将一个脚本中的类与另一个脚本中的类相关联,因为在下一个请求下,它们之一可能会更改,并且会出问题。 此外,在几个文件中可能有一个同名的类,在一个请求中,其中一个被用作父类,而在另一个请求中,则使用另一个文件中的另一个类(具有相同的名称,但名称完全不同)。 事实证明,当生成将在多个请求上执行的代码时,您不能引用类或方法,因为它们每次都会重新创建(代码生存期超过类生存期)。
预加载使您可以初始绑定类,并因此更优化地生成代码。 至少,对于将使用预加载来加载的框架。
该技术不仅有助于类绑定。 在Java中,类数据共享也实现了类似的功能。 在那里,这项技术的主要目的是加速应用程序的启动并减少消耗的内存总量。 在PHP中获得了相同的优点,因为现在类绑定不是在运行时完成的,而是只完成一次。 另外,相关的类现在不存储在每个进程的地址空间中,而是存储在共享内存中,因此总的内存消耗下降了。
使用预加载还有助于对所有PHP脚本进行全局优化,完全消除了OPcache的开销,并允许您生成更有效的JIT代码。
但是也有缺点。
如果不重新启动PHP,则无法替换在启动时加载的脚本。 如果我们下载了某些东西并将其永久保存,则无法再卸载它。 因此,该技术可以与稳定的框架一起使用,但是如果您每天多次部署应用程序,则很可能对您不起作用。
该技术被认为是透明的,也就是说,它允许在不进行任何更改的情况下加载现有应用程序(或其部分)。 但是在实施之后,事实证明这并非完全正确
,如果使用preload加载了所有应用程序,则并非所有应用程序都能按预期运行 。 例如,如果根据检查
function_exists
或
class_exists
的结果在应用程序中调用代码,并且该函数分别变为常量,那么
function_exists
始终返回
true
,并且认为先前调用的代码未被调用。
从技术上讲,仅通过一个配置指令opcache.preload启用预加载,然后在该输入的输入中提供一个脚本文件-一个常规的PHP文件,该文件将在应用程序启动阶段启动(不只是加载,而是执行)。
<?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework");
这是递归读取某个目录(在本例中为ZendFramework)中所有文件的可能方案之一。 您可以使用PHP绝对实现任何脚本:读取列表,添加异常,甚至与作曲者交叉使用,以便它预加载所需的podsoval文件。 这全都是技术问题,更有趣的不是如何运输,而是运输什么。
预加载中要加载的内容
我在WordPress上尝试了这项技术。 如果仅上传所有* .php文件,则WordPress将由于先前提到的功能而停止工作:它具有function_exists检查,该检查始终为true。 因此,我不得不稍微修改上一个示例中的脚本(添加例外),然后在WordPress本身不做任何更改的情况下就可以正常工作了。
结果,
由于预加载,我们获得了5%的加速度 ,这已经不错了。
我下载了几乎所有文件,但其中一半未使用。 您甚至可以做得更好-驱动应用程序,查看已下载的文件。 您可以使用
opcache_get_status()
函数执行此操作,该函数将返回所有OPcache缓存的文件并为它们创建一个列表以进行预加载。 因此,您可以节省3 MB并获得更多的加速。 事实是,需要的内存越多,处理器高速缓存就会变得越来越脏,并且效率越低。
使用的内存越少,速度越高。FFI-外部功能接口
为PHP开发的另一项与JIT相关的技术是FFI(外功能接口),或者用俄语调用无需编译即可调用用其他已编译程序语言编写的函数的功能。 用Python实现的这种技术给我的老板(Zeev Surazki)留下了深刻的印象,当我开始将其适应PHP时,我印象深刻。
PHP已经进行了几次尝试来为FFI创建扩展,但是它们都使用自己的语言或API来描述接口。 我在LuaJIT中窥探了这个想法,其中使用C语言(一个子集)描述了接口,结果得到了一个非常酷的玩具。 现在,当我需要检查某些东西在C中的工作方式时,我用PHP编写了它-它发生在命令行上。
FFI允许您使用C中定义的数据结构,并且可以与JIT集成以生成更有效的代码。 它基于libffi的实现已包含在PHP 7.4中。
但是:
- 这是1000种射击自己的新方法。
- 需要C知识,有时需要手动内存管理。
- 不支持C预处理器(#include,#define等)和C ++。
- 没有JIT的性能相当低。
虽然,也许对于某些人来说会很方便,因为不需要编译器。 即使在Windows下,也可以在没有PHP的Visual-C的情况下使用。
我将向您展示如何使用FFI为Linux实现真正的GUI应用程序。
不用担心C代码,我自己大约在20年前用C编写了一个GUI,但是我在Internet上找到了这个示例。
#include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; }
该程序将创建应用程序,挂在activate回调事件上,然后启动应用程序。 在回调中,创建一个窗口,为其分配标题大小并显示它。
现在,用PHP重写了同一件事:
<?php $ffi = FFI::cdef(" … // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app);
在此,首先创建FFI对象。 接口的描述作为输入(实质上是h文件)和我们要下载的库发送给他。 之后,接口中描述的所有功能都可以作为ffi对象的方法使用,并且所有传输的参数都自动且绝对透明地转换为必要的机器表示形式。
可以看出,一切都与前面的示例完全相同。 唯一的区别是,在C中,我们发送了一个回调作为地址,而在PHP中,连接是通过字符串指定的名称进行的。
现在,让我们看看界面是什么样的。 在第一部分中,我们确定C语言中的类型和函数,在最后一行中,我们加载共享库:
<?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ...
在这种情况下,这些C定义从GTK库的h文件复制而来,几乎没有变化。
为了不干扰同一文件中的C和PHP,您可以将整个C代码放入一个单独的文件中,例如,名称为gtk-ffi.h,并在开头添加几个特殊的define'ov,以指定用于加载的接口名称和库:
#define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0"
因此,我们在一个文件中选择了C接口的整个描述。 这个gtk-ffi.h几乎是真实的,但是不幸的是,我们还没有实现C预处理器,这意味着宏和包含将无法工作。
现在让我们在PHP中加载此接口:
<?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } }
由于FFI是一种相当危险的技术,因此我们不想将其交给任何人。 让我们至少隐藏FFI对象,即在类中将其设为私有。 我们将创建一个FFI对象,而不是使用
FFI::cdef
,而是使用
FFI::load
,该对象仅读取上一个示例中的h文件。
剩下的代码没有太大变化,只是作为事件处理程序,我们开始使用未命名的函数,并使用词法绑定来传递标题。 也就是说,我们同时使用C和PHP的优势,而C则没有。
以这种方式创建的库可能已在您的应用程序中使用。 但是如果它
只能在命令行上运行 ,并且将其放置在网络服务器中,则会在每次请求时读取gtk_ffi.h文件,创建并加载一个库,完成绑定,然后再进行所有这些重复性工作,从而加载您的服务器。
为了避免这种情况,并且实际上允许在PHP本身中编写PHP扩展,我们决定将FFI与预加载交叉使用。
FFI +预加载
代码没有太大变化,只是现在我们将h文件提供给预加载,并且我们在预
FFI::load
直接执行
FFI::load
,而不是在创建对象时执行。 也就是说,加载库,所有解析和绑定都完成一次(服务器启动时),然后使用
FFI::scope("GTK")
在脚本中按名称访问预加载的接口。
<?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } }
在此实施例中,可以从Web服务器使用FFI。 当然,这不是针对GUI的,而是通过这种方式,您可以编写(例如)绑定到数据库。
可以通过命令行直接使用以这种方式创建的扩展名:
$ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");'
FFI杂交和预加载的另一个优点是可以禁止在所有用户级脚本中使用FFI。 您可以指定ffi.enable = preload,这表示我们信任预加载的文件,但是禁止从常规PHP脚本调用FFI。
使用数据结构C
FFI的另一个有趣的功能是它可以与本机数据结构一起使用。 您可以随时在内存中创建C中描述的任何数据结构。
<?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y);
100 ( FFI::new != new FFI), integer. , C. PHP, . count, / foreach . 800 , PHP PHP' , 10 .
FFI:
Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).
, FFI .- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .
for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } }
FFI .
: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT)., ,
.
PHP Russia 2020 ! telegram- , 2019 youtube- , , — .