EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
前言:众所周知对于循环这种运算,matlab的运算速度是不快的 想起来个冷笑话,黑客帝国里的主角呢奥之所以能快过子弹,因为Matrix是matlab写成的hhhh 因此将循环部分用C语言写,然后编译成matlab可以调用的mex文件,无疑可以加快循环运算速度,从而加快整个代码速度 之前因为合并数据需要不会数据库,强行在matlab里利用类似excel的vlookup功能,无奈太慢,只能想办法最后知道了这种方法,最近偶然在桥哥的知识星球里又提到了,也顺便复习和记录一下。 ps:今天在闲鱼上买了小车车,高兴! 1.环境配置我的是matlab2019a,win10-64bit,c语言编译器我选的是TDM-GCC(gcc/g++),安装起来很简单,推荐matlab2015及以后的选择这种,详细安装过程可以见文末链接1中的For Matlab 2015部分
; t; x; X s: b1 o# z x
2.如何编写可以被编译成matlab可执行文件的c语言代码我是在matlab中文论坛(地址见reference第2条)里找到如何编写的,不过他写的有点乱,我来消化后写一下自己的理解 mex接口函数跟一般的C语言文件主要是两点区别, 第一是必须引入头文件mex.h #include "mex.h"第二是多了一个叫mexFunction的函数 void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])举个栗子: - 9 C0 s) l( p! g2 T4 _9 l6 M- C3 [
% I" n4 t8 v( S#include "mex.h" // 使用MEX文件必须包含的头文件
; |" R' N" a; |& h- T$ A; b' M0 U+ ~0 [6 x- i7 N! f0 _0 h
& S- D* L- J* T3 M
) g1 O; c, H' L: ^ j' T6 Q// 执行具体工作的C函数3 H& a$ H- K) \3 W
: l" g& i4 e/ P6 e4 I6 n, Z
; E q7 r; w; C; h; e" c
$ ]3 ?. c- I2 g4 I; P$ X- _double add(double x, double y)
' N5 V4 r( M$ L+ j! F8 s# X
, {- c" ]! m1 o; c7 k. I- + R0 D5 O5 e% i H8 W
' i. ?5 ?* y# \% j4 m8 `{
* c z: W$ _2 I
$ K ~3 j. ]1 r. Y' ^2 s0 D4 |8 P - ) r0 j; i6 {7 P b; _5 [
3 P! J+ \* w7 j5 ]9 b
return x + y;; a4 [; f& f" S7 Y# V9 @# B
; c, {9 ?1 o3 m& d; P
; }' E. d- P9 `3 }
$ g' W$ a9 @( Q# {" v}' b1 u1 _# b$ d! E `$ @2 k; R& a9 p- S
! t; g& H$ b2 p# c
' a4 f5 h0 ~( K. q( V
1 g* K) ^% H, y// MEX文件接口函数
# g% ^- x( }# h7 o$ s, f6 o2 a) ~2 t* L" i: W i6 [. z
5 h5 } h( w# p* s# g2 C
! z% I( I; O5 J) X$ Rvoid mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
/ ?; A0 f* t2 V% k5 I5 Q) b
+ s) x: X% D+ `4 o" a- " X: u8 P$ c0 `6 r/ M# `
9 e2 a: F9 q; A( b1 v3 O{
& u1 f, n* b6 v2 G4 Z p: r! e5 P; K; f9 U& Z. k
- 1 u! r; r0 d! E( x% i
0 x8 J& D( d( o0 h% X
double *z;
w3 y2 t0 s. b% m/ E8 a5 }0 ^6 B0 H9 a+ D! n$ M( O6 t7 h
- $ }( L% E& I$ R$ S! Z
9 x/ l& |0 I2 z7 Y double x, y;) J1 n. G# Q. m2 f% l
/ I$ i2 i; ~4 o) A/ |
7 D' x3 o6 `7 v" A- a. q& y$ }" ^ e6 B8 _; ?
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);- F! A& d; b, t! a( I
; t) U& Q; u) W9 S2 L) K
! i5 s( J4 y1 s# ~$ Z7 p* F
$ W% |' m9 L/ L4 Q z = mxGetPr(plhs[0]);: k5 c" b9 V% T0 Z- h- v/ E; P
; N3 T; {9 {% A5 ^8 j9 b8 Y
8 V" ?- i$ V; t1 ~8 r+ R. }! X& @7 X; M1 k; C- @3 R& B: y
x = *(mxGetPr(prhs[0]));
& v0 }, c- x6 Y8 ^" K/ y. D! ^
/ X) o4 p7 x" S. T C7 ~+ t
; `" _# |4 M) N$ K1 U; @3 L9 A* I7 w6 n% i8 |+ `
y = *(mxGetPr(prhs[1]));4 A! U3 S* [& d1 A, r! R# {3 Q
/ c7 H! e$ Q5 e3 Q/ b3 T
) P8 C7 i7 W0 |' Y6 y+ o1 M* s- ]* B8 D
*z = add(x, y);
( w* H1 x, g; ~/ ?, F9 r2 s9 l S3 I* _6 C& K# F& G
) j' e" j5 D- W% x, l
& T! _- y9 ~5 v/ E} _7 o) O5 q( a' v) O
+ o+ V4 l- w: {! {8 E/ e
1 i! u' {# J+ [- a& E, c
也可以写成: - : I6 s1 {: Z! L* f1 ]# E7 N4 ?) a
& o2 w0 H( r5 O# H
#include "mex.h" // 使用MEX文件必须包含的头文件
( C" e7 g2 U) ~; }% O1 ~! S3 _% n% @8 }) R
4 s9 t s, u; l ?0 a- X
! m |. j4 y' V$ j* r' r, M// MEX文件接口函数6 Q0 P, _7 K$ d" t4 V% Y1 W- z
. o% ], n2 m' {& [0 T; d
/ l6 n, W& T9 i* a r* |$ z7 k, `& s( e6 ^( ~, ]) N- T; G. M
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
D: N @4 k) `" b
* T9 P9 y4 v/ g9 J- ! \' v$ U, h2 W) K' M) f
a8 t2 `) Y! b: K% c* S
{
; x- y2 p/ u; G4 k- v& t" L _1 P: S+ \
- ' D( o8 X: R4 a ^1 Z
; d2 p" t0 Q0 m4 r" r double *z;& G6 X3 ~. h4 o3 R* O
& x- ^- e- w9 ~$ ~# ^" { - # N; a" y+ F% Y* e1 C A. L9 t
3 S, `; q& e0 T# b double x, y;
2 ?% ~5 Q! f0 r( r2 ~+ J8 O$ T( L- C1 X" I+ T
- - Y# R- e! ]- R; G: a, O' n5 t
- {1 X1 C1 X& y: _/ [% n# i6 T, Y8 C plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);- {& v) i6 S2 N% j
$ s1 N" U5 X. ~) A. y6 o
- # d% p, U% \, @5 W) l* \, k, ^
- S2 V0 r+ u. r9 d
z = mxGetPr(plhs[0]);2 t) B2 }0 P# f& d
+ r# ]* M5 i; ^+ E
2 e3 {/ S+ X4 G: ^! c5 h: c4 {& y! q- H8 h: Y& I
x = *(mxGetPr(prhs[0]));1 X' ^9 q, s! A
; I5 t7 a" n5 I$ ?; l2 A9 g
2 ]8 ^+ }( D" T: c2 a0 k( E3 @: `8 d* ^) p9 n: ]; E
y = *(mxGetPr(prhs[1]));
+ H. O( }5 o& m- e% Q4 z1 b) H. [2 C. G- y8 z2 H6 i. D
- : D& j) D, |7 ]/ X5 V7 `
' d2 z; K/ ?; S3 M. P' l; J *z=x+y;
0 X- Z% g9 N& j4 Q6 k" D" b( u# [
( }9 A5 I+ H1 {% T4 A/ y. V. d! p2 `1 F, m) y% Q# X8 L
}4 [5 v/ T: d( p/ R
8 }/ z& k K' R9 K& l, ?$ z: S
P# O- X0 {5 }$ x* I. R* x6 M
也就是说执行具体功能可以通过编写一个独立的c语言函数在接口函数里调用,也可以直接写在mexFunction接口函数里 我个人推荐将具体功能独立出来,这样一来: c语言函数负责执行具体功能 mexFunction接口函数负责数据的输入输出(将matlab数据输入c语言环境,运算后再输出回matlab)形式组织 再调用c语言函数
分工明确。 于是关键问题就是如何编写mexFunction接口函数了 void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])下面解释一下mexFunction各个参数的含义(这里看到是void,即无返回值,因为传值是通过指针数组plhs[]传出的) 参数 意义 英文全称 类型
0 a' ?+ X8 Z* J& Anlhs左边输出参数的数目number of left-hand side整型(int). |. }6 _+ i, y6 e2 ]$ i: z' C
plhs指向输出参数的指针pointer of left-hand side指针数组
+ O: W* f7 b" A" }: D" Xnrhs右边输入参数的数目number of right-hand side整型(int)
. A" _9 T0 l) p& D+ T9 nprhs指向输入参数的指针pointer of right-hand side指针数组% M! G b0 @0 n# j, N
第一个参数nlhs是输出参数的数目,第二个参数plhs是指向输出参数的指针 第三个参数nrhs是输入参数的数目,第四个参数prhs是指向输入参数的指针 其中plhs和prhs类型都是指向mxArray类型数据的指针,prhs这里还加了个const,这里的const跟之前c语言中是一样的,代表不改变,因为prhs是指向输入参数的指针,mxArray这个数据类型是在头文件mex.h中定义的,in fact ,在matlab中大多数数据都是以这种类型存在。 还是拿之前的栗子来解释:
6 ^5 ^) W0 j* y; V. \
) b: _3 t6 R6 u#include "mex.h" // 使用MEX文件必须包含的头文件 l5 P' X( P6 q3 [
0 G" C4 V+ Q8 U/ T' @: Q" g
# t0 s2 I7 u& @# [
% ?4 l. s) ~: y$ T6 H/ S; @ D// MEX文件接口函数
# J% {# b! J2 S
# D: r4 {. }1 @# ^% ^! Z- ) Q# m6 t3 O s# U i
% h# [! i0 d# G. ] Z _! J
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])4 Y# X. h: m* H! a4 k
6 R6 M* l V+ T, K7 F4 E
- " n0 m. i) p3 Z q) }7 [
* ]; O4 P; L3 m* G5 X: V/ X" L{$ k% ^$ J% R7 j m
( O2 G6 h; A; \$ X# H4 R0 {3 ?8 H
# ~, G ^( N% n2 z ?3 W @. E# s+ L+ ~3 e4 Z3 c! ^, G* H7 s) h
double *z;
) |# n7 m$ i$ y+ }& R9 e$ ~& ~) |5 F+ v8 n" J8 U5 u. ^" K- t
- & c8 J; I6 o; c2 c9 i9 X0 o8 D
$ m; j+ N6 b! o* g* a u5 A double x, y;
/ l. O7 {8 t4 f
. m: K, I1 Y9 x! R g2 Z% k
t5 I; ~/ b, r A9 O1 {! d' [% x* _! }; R
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);- E2 E, E# q* \ Z, Z( Z
4 f3 m: ?4 I5 Q! U; l1 R
- & x0 r( U& F# }8 p( \0 e7 w: J/ j
! a; M7 h, G* N5 f9 P% M z = mxGetPr(plhs[0]);7 F6 Z3 _" `3 m" a# e
. y a. {; E, p) W
2 ~% P' U u i' a
+ [! c: E6 i ] x = *(mxGetPr(prhs[0]));3 s4 G. v9 S- o2 I
2 G; i/ B7 |/ ^
! }* }$ f2 u* i+ U" P/ }3 }# w/ F- [1 |0 a& b |, h2 b
y = *(mxGetPr(prhs[1]));9 ?* T) j9 F9 t+ ~6 J: ^
2 \. c4 e% k- S
w% x" n6 ]9 E. R; E7 ? W
0 r6 v+ R$ P# G& b6 N0 H+ Z *z=x+y;
( R4 F: U: F; f8 Z: s7 v; ?+ B
. v. h9 S/ W9 z" w0 w- & g. Q) U+ {% s" {$ a+ M
1 u2 _2 R; Y1 K, p# o2 s3 `- Z k
}
9 ^5 v* g7 o" w5 ^" |0 E, f4 R! v8 ]3 b4 K. w* @' W* @
8 |$ f- r5 w2 N! I! q/ @& E
在这个栗子中,输入参数是x,y,输入参数是z(虽然没有返回) plhs和prhs其实都是指针数组 prhs[0]代表指向第一个输入数据的指针,mxGetPr代表获得这个指针,后面再加个*就是c语言里面再正常不过的按指针取内容了 所以就这样x,y通过下面这两句传入到了mexFunction中
( V8 z( P- j/ D4 S3 J
$ S& Q; ]; u( ]1 |x = *(mxGetPr(prhs[0]));+ w% c+ }& R. K
* i! t0 q ^, K: Z
- 9 z9 ~" S7 l) l
' g% m8 G( q( s( C5 F% Zy = *(mxGetPr(prhs[1]));) W6 {8 o* H) s5 c) q
6 a0 F7 {# ?( M3 ^
4 G+ h) E; [# r% S8 K1 T* L
mxGetPr函数的功能是从指向mxArray类型数据的指针prhs[0]、prhs[1]中获得了指向double类型的指针 另一个函数是mxGetScalar,Scalar即标量,功能是把通过prhs传递进来的mxArray类型数据的指针所指向的数据(标量)赋给c程序里的变量。前面的mxGetPr是传递矢量的,否则矩阵或者向量就没有办法传进来 这里由于传入的每个参数是单个值,于是也可以用mxGetScalar改写:
% R$ y$ U# F! c* P9 V! i& E$ S7 c; q0 E. J
#include "mex.h" // 使用MEX文件必须包含的头文件& a2 J6 A+ C$ c8 S8 U
6 i. J/ ~, }# b) _( F9 A- }
" I1 \( S* T: z' k+ v# \ t( I
* Z* l6 l- u; o E/ o// MEX文件接口函数3 I$ C5 P' ?: I1 c
4 g- F8 P K- o& C- ( z' w% D# x- P- F/ K
( h# F, H) d9 l; x: avoid mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
* c# Y8 g1 Q& Z% F0 T7 b7 T9 N3 I P, t- \6 E0 f4 O( W( U
3 f5 k3 r' E, c! l1 S; Y
% n0 M0 B) q8 i+ b; P* ?9 D* {{, F9 u. H$ A: d/ |% j$ H
* G3 V) M& P) i0 | O
- 4 d% S4 b; l2 l# Y, O1 I" E; M
; j' w8 p: z* B3 p6 J
double *z;% U) c2 m l) Q) l5 }( I
( `! y/ ?, Y/ u( r: x2 W
6 m& `" p; k/ N6 p1 t' v1 @0 e" R& }. p
double x, y;
+ F2 e# v: T+ |
2 z/ ]3 w' C1 {0 |, p7 D- 2 h6 c2 T- {- ~. V7 \
1 N+ E# |7 g# C9 G plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
) H: |# \7 O$ x: o' @( x+ _# }$ r2 f, {
( Q+ y$ _/ B9 ~1 | - ; ]5 [$ R: u) H' x l9 b1 ?3 [
3 O9 Y9 |) @* N/ a
z = mxGetPr(plhs[0]);
+ U' M& e( }8 G/ ]4 p1 u+ j
- c+ ^& a0 o0 y - ' }& `% A) w& ~
; S. R$ e& P, ~: K7 ^' H' }; q9 b x = mxGetScalar(prhs[0]));
' Y/ q. I) }" k5 x1 u8 i0 e! q8 |* G$ ~" s
- ; w2 q. v: F$ T% [
+ s* A7 m; t* P: n
y = mxGetScalar(prhs[1]));
R- ?# k8 z/ Z0 k
; g7 N' w6 `0 Q# ? - 5 h- | t* g5 g* s- h: r7 L0 @" u
+ a% [8 }- y$ k- m
*z=x+y;
8 {8 Z9 V, e, r. t% B9 D
2 n0 d. |0 t" s, t9 ~
' j& b2 r4 e: D+ Z& U8 X# J" T1 K+ K
}
9 |3 n# A3 K) |' Z. F( k
6 z6 {7 B0 F0 n
+ z4 [# z* Z) }" V6 \5 n
(注意:输出参数因为需要通过指针传出,因此这里z必须用mxGetPr这个函数来从mxArray指针获取double指针) 但是这样还是有个问题:如果输入的不是单个的数据,是向量或者矩阵,即使我们通过mxGetPr获得了矩阵或者向量的指针,但是我们假如不知道矩阵的shape,还是不好取值或者计算,所以这里又有两个函数mxGetM和mxGetN可以通过传入参数指针获得传入参数(矩阵)的行和列数。 需要注意的是: 在matlab中矩阵的第一行和第一列的序号是从1开始的,而在c语言中是从0开始的,而且在c语言中的一维数组存储矩阵的数据时,是一列列从上到下,从左到右来存储的,也即 matlab中一个m*n的矩阵M(i,j)对应于c语言中一维数组N中的N[j*m+i]
举个栗子: - - J A1 \' ]: w: D9 k1 v4 T. Q' N
$ S1 U2 u9 m; ]1 ]2 C( E* |- L#include "mex.h"$ W7 T3 r& ]4 d! `
5 v) A6 W1 Y( @ - 4 J3 W, H* z2 Y5 U6 o' z) @4 q# }
' j9 X- k# D) G
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]){+ N( [" f8 f! {0 E% x" x+ P6 V! U
7 K- g1 }: Y2 g! s3 e6 n$ [: [ - * T5 Y$ A" \( n) W+ g% h' {* b0 U
6 Y1 G, F+ U- h+ d D
$ `. m$ G4 R* X, H& x
) d) v+ p. K! p% s
5 G5 M( h1 I0 N# i
* M9 [/ I4 `# M% ]4 ?5 X5 wdouble *data;/ T% M+ r6 O4 u. q# b* s
2 u: j& X& t3 X/ c) W5 U0 V- : U: S Y: e5 P2 m2 Y
9 ^$ B' I* G1 Y ?int M,N;
7 V) y$ h1 t( g2 T; R* [. x+ g Z0 m; u l% N4 C% `
8 \2 k2 M# B A1 e* y" f! z* X) b4 b# i: z! V2 K% v; I( @! I/ u7 O
int i,j;
* m' |# f7 G( W* A* R: s! ` B7 l: i% p
, F1 j, {: `+ D0 g
2 m& M# Z) |9 G1 bdata=mxGetPr(prhs[0]); //获得指向矩阵的指针) o' k6 a2 K5 G+ e7 z6 h
" }$ E6 y; Y1 a7 J5 U2 s: r
& s& b) M a/ d1 |+ H9 T9 `
2 q+ j# x) M: ]) R. rM=mxGetM(prhs[0]); //获得矩阵的行数! G* b+ O5 Z8 } {
9 E$ `" v9 x( R3 p- 4 a& r" H5 T$ D0 c
4 p( }/ K9 ~! E! K1 P
N=mxGetN(prhs[0]); //获得矩阵的列数
- E$ U" h5 a- d; {, a# f- T1 o9 ?% O: a8 m' q" \
) D% A' |8 v% c0 m
2 o2 f- y5 P0 d8 K, d% j( o' Ofor(i=0;i<M;i++)
+ f) N1 n6 M2 Y: L
0 _* K1 M _" ?3 x- 6 S" {/ s5 m+ e
3 b* A' Y- u- G4 S" @" i{
; P0 V% L% F O! t2 M1 D7 f6 f) D7 c' W& }6 R; |8 x/ e
- ' l$ Y- I; @! i- B" ~: A
* ^- P& U. K- W( u* [; h
for(j=0;j<N;j++)
. b v4 G1 \ F0 H/ t4 z/ l0 x; L! m. K1 m2 \. ?( R
8 A3 L' I( S# j+ z& h1 S
6 x! |1 n& v7 R0 G9 L) g0 G& v mexPrintf("%4.3f ",data[j*M+i]);
8 s' p1 |: m \" U! f8 E. C: Y5 H) R/ {! D. ~+ I( h$ ]
9 G9 ~. p3 k1 r, c! R- R
- T/ Q4 a" T- q4 b' @5 e mexPrintf("\n");
" s4 x. i; J7 z: H9 L6 D
3 a, F4 N& T7 [, p9 K) l- d# }- ) T8 v4 B2 g. o0 |8 Q* }6 x
7 N: e1 E$ F9 r
}
: d& r9 \& v- w T7 N4 e! N% _0 t! A' M4 o& }4 T* u1 h6 j+ I
- 1 Z8 u _' a V" q& j. q" a
5 @9 a; r$ [( X# `, Z- s/ k }% N' P- s1 m* r% D/ m* ?/ i
5 v2 {; r6 q: I; {
, d" U+ v Y/ e( `# P3 \" o# L5 R/ Y
假如是一个形如[1,2,3;4,5,6]的矩阵,则执行上述后会先后打印出1,4,2,5,3,6 以上讲的都是输入参数,由于输入数据在函数调用前就已经在matlab里申请过内存,由于mex函数与matlab共用一个地址空间,因此通过prhs传递指针即可传入输入参数。但输出参数却需要在mex函数里申请内存,才能将指针放在plhs中传递出去。由于返回指针类似必须是mxArray,所以matlab专门提供了一个函数:mxCreateDoubleMatrix来实现内存申请,函数原型为: mxArray * mxCreateDoubleMatrix(int m,int n,mxComplexity ComplexFlag)m,n分别是待申请矩阵的行数和列数,需要注意的是为矩阵申请内存后得到的是mxArray类型的指针,就可以放在plhs[]中传递出去了。但是对这个矩阵的处理(包括赋值)却需要在mex函数或者c语言函数中完成,这就需要通过前面的mxGetPr或者mxGetScalar。使用mxGetPr获得指向这个矩阵的double类型指针后,就可以对这个矩阵进行各种操作和运算了。 还是拿这个栗子讲,这里由于输出是一个数,于是m,n都是1,通过mxGetPr进一步获得指向double数据类型指针z - 0 d- k# [8 ^; O- r
) \! N- ?, ~, a* m% ~: s
! \& m; v3 C" ~( p" ^2 Q3 U ?& E! h; x d0 Z0 z5 d
- 7 t6 @! f0 W0 {
: s/ h0 L5 f/ E9 M' _- ` double *z;
2 a" T: O E/ @: f: c- d7 B3 G4 p, i; Q9 a- y+ X* b% |
7 c* k2 U$ D! C1 B' E+ I& P( a, D$ \" N; `5 L6 ]1 _& e$ [9 O& B% M
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);; ^2 A9 g! d( \
2 M9 O2 `% b+ _$ |; G9 ?
, W8 W# e$ L! @
! M, H, l" a6 m3 k* |7 z# s z = mxGetPr(plhs[0]);
4 j6 f: \- h" a' s8 s! m) Z; B7 T/ e- K8 r
|8 [# ^- {. b( d; }/ J1 ~% Y' u; Q1 I T9 S" o d
x = *(mxGetPr(prhs[0]));
( Q/ Q- h, ?3 N# [0 y D2 d3 c. C+ v
- $ _9 h0 N F7 ^% m/ |2 H6 U
: I8 { n: K, @1 {9 b# v, d9 k
y = *(mxGetPr(prhs[1]));
' X. C. r; b) T3 X7 ~
6 v5 c# J s- c, m - 7 c9 Q( c8 J8 Q0 }2 L m8 K/ R
* ?' Q0 j7 K. p' h/ m4 X4 N$ s0 Q
*z=x+y;8 n) R# }5 l; e
! |. w3 E3 K( [& @5 i
8 F, a- B2 E# h& {1 j# [0 i+ v
当然,matlab里使用到的并不只是double类型这一种矩阵,还有字符串类型,结构类型矩阵等,并也提供了对应的处理函数。 ! c7 M5 E, K& j# d- G5 b$ U# I
|