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部分
) _# V9 D# r) L) w. k( e3 [ Y2 P3 M
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[])举个栗子: - : T! X4 Q* F% R! L; s6 ~( A
" Y; l- N% C4 B8 G
#include "mex.h" // 使用MEX文件必须包含的头文件
/ i; l. U: I! X. E. O) v) J5 n" ^. ~& p! a( V
- & p- D2 F2 d8 N( _7 p) y+ x
" H7 z/ u& _- O
// 执行具体工作的C函数
$ c8 O1 N$ x4 T' a- p) ^0 k! j, |" E
- 8 w' B: M4 a2 n* n
+ e$ J* i+ Z' R) i$ {% U0 zdouble add(double x, double y)
7 u' K0 _0 u" K# @( F4 N
- K6 c. i% E. `* h9 Y; R8 m0 P: b - + R& r- d- u9 r2 z9 A, F, f
# u) n5 o4 ~2 N0 c* `# q' V{0 V$ O. R, z$ z3 H7 a, w4 V4 ]( R0 W
/ r, A" g- n7 ? s2 r: S( S
- 4 k B3 U" |, z7 j4 R
, {1 I7 Q) k' r) b" j
return x + y;: n4 Z {1 o. _' B" ~
; k; S- u, D% `8 b
. Y# T# O# a( J' e6 q$ k; u$ g# {# R9 ?4 C# C
}
8 M7 e, e* u5 j0 {
* M& _' Q6 b4 H+ c6 e7 C8 y( k" k
# @/ U; j! i+ N G. N' n& j7 Q
/ Q2 D( d: c Z8 _* `* r// MEX文件接口函数) x- c* W4 x5 g) g" Y Y8 E
0 \0 Y% z% p9 g) P
6 q: ]! f5 k! {5 l2 H8 m
8 {6 s' V9 J W8 R/ Z; Wvoid mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
* _3 G# _. l4 f3 i2 ` w9 K3 B7 |, w4 W$ S; Z
- 0 S; w* k% t2 b; A
: y1 E& F, S0 I. P) L( _: Z& A( y
{
, v( ~- u# `+ P: e/ H
, v' I9 }: t C+ C) I$ N
7 [& p/ M5 h3 P0 V, v2 G
1 Q% d6 N |, j3 Y* o: P double *z;8 ]) u% a0 B9 Q6 B5 V- o
; e# F2 B* ?: ^: M5 K
; c4 q+ y+ s* b, J( [
+ [/ h% D& ~8 Q4 [3 l double x, y;! |3 z: W0 r0 U* f$ D1 p% M
' S- n5 j1 Y" j& f
8 b8 g& J1 e" \. Z& I
{. \1 Y O: s# ]# E! d# D plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
1 O7 r0 e: W l; ]% j4 B, A
7 p* |) y' @7 m- & c+ H& N0 K8 }9 j
b1 n# I5 O! h6 J! B2 P; y% v4 T
z = mxGetPr(plhs[0]);
/ }2 z% g5 P8 I* u0 i, N7 z( p* b v* D/ [
/ i2 r; q2 S& H3 g - 4 E0 l" A, n; K
: q; a' @3 |: k* x
x = *(mxGetPr(prhs[0]));8 x9 J- G% R. {! e" H' K
: E" j! t) k" D5 w$ N& g* X* q, M
7 X0 G2 E% Z: u; @, A2 I- X
& C& {* n0 n5 r8 g y = *(mxGetPr(prhs[1])); h" V; c2 h( n+ {7 o
1 r3 [- W$ ~- \) n
- 0 T; H T1 f5 c- s8 o4 J
$ a7 n) W# q" b+ J4 Z. b$ D. n( j *z = add(x, y);9 ^" m! U' l2 v* [5 |$ @
3 e, Z6 p" E$ l! L
- 0 W" m1 B1 b0 q1 ?8 e* y
% C f; ~% C# K7 V. a}
$ x, u% F$ v3 y0 c! V4 V. A% w8 j- X9 A @3 T, b, c
) N! F; `4 L4 ]# ^! J
也可以写成:
( }3 I R* s9 d; F. O) ?
j( A2 l- M! X y M3 G#include "mex.h" // 使用MEX文件必须包含的头文件
) R5 h: d$ X: w7 L# \8 ~% c- N7 W' U& Z9 K
- 1 }" M4 S6 P- j+ X. Z
+ l/ V1 e& A1 D' u, M: Z- ^// MEX文件接口函数
% Q* h; T' b0 ^6 o* t3 a; c4 k( |& q
& w% y* U7 x7 N: R X - 5 s" \- p( R) d
# t0 s1 u+ d4 J* b- g( r4 H
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
9 g& j: o- M0 v% B2 I5 E# V; K0 [1 @/ \5 x1 f5 _5 ~
- 2 P: j9 D- H* R2 [* w! a5 H
8 o% e) U# p# [- U6 g
{5 z- J2 w! K& S9 G* y, A K
5 L; I1 R; t$ B2 i) i: E
0 h, H! b+ M2 T8 ^: ?4 i
! o" X* J. ^9 F% F double *z;
5 _$ b9 G) K* U6 n: O' @ K" z
! z0 l0 n' S* ` K& D- ) s& ~5 @# U4 q1 W m: G
) {. _* W& q, p) u double x, y;
+ e, \9 T, a% Q/ @3 K3 ~ }6 O0 X
- R' O# B6 A2 K: r# E
( C5 T( U. `% h6 b) Q plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);; Y+ h& v: I( \2 Y6 i; O- q" b- _
/ L) b0 v. D* V( k1 {. z* E
8 ` |% v( Z# ]" I
& v# N. ?: k4 e7 W) _% n: n z = mxGetPr(plhs[0]);
) S. G; q5 e0 ]
0 V& A/ x' ?2 J& q3 ?4 I: o- 4 {& e6 R, q/ N( w
8 T3 Z. T# O; t7 O
x = *(mxGetPr(prhs[0]));8 |7 s& _7 \* O6 J
7 g4 R& V5 I$ y, B% J/ s8 c8 z
- " j! p5 \, z* J5 r
! t, L+ E& r- e1 q8 m y = *(mxGetPr(prhs[1]));& G& p: H; _* a; E. j
( K, m* f6 @& h! a0 f
- , V/ R8 E x. Y- G, J
( p7 ^, \( \4 ~( o# i
*z=x+y;- }4 R @' U" G, \
6 Z) ~$ U# g% c4 q+ j/ { - + o( E0 m$ l# V& d: j; [* n
. a5 W' z) G) ^) L2 }# L
}
! g. z& Z. t8 J7 R! W" j
5 {8 \9 X7 w$ b1 f
( T" I" z9 w1 [& J0 P0 y
也就是说执行具体功能可以通过编写一个独立的c语言函数在接口函数里调用,也可以直接写在mexFunction接口函数里 我个人推荐将具体功能独立出来,这样一来: c语言函数负责执行具体功能 mexFunction接口函数负责数据的输入输出(将matlab数据输入c语言环境,运算后再输出回matlab)形式组织 再调用c语言函数
分工明确。 于是关键问题就是如何编写mexFunction接口函数了 void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])下面解释一下mexFunction各个参数的含义(这里看到是void,即无返回值,因为传值是通过指针数组plhs[]传出的) 参数 意义 英文全称 类型 4 d( c8 r' }8 R! P$ k
nlhs左边输出参数的数目number of left-hand side整型(int)+ _" h% p% ?4 b+ Y8 j* ~
plhs指向输出参数的指针pointer of left-hand side指针数组
( ~6 S5 y0 ~3 N! C+ gnrhs右边输入参数的数目number of right-hand side整型(int)
1 c4 @+ _3 J, R1 \, L! jprhs指向输入参数的指针pointer of right-hand side指针数组6 j4 l, \' M+ M" l) }( J
第一个参数nlhs是输出参数的数目,第二个参数plhs是指向输出参数的指针 第三个参数nrhs是输入参数的数目,第四个参数prhs是指向输入参数的指针 其中plhs和prhs类型都是指向mxArray类型数据的指针,prhs这里还加了个const,这里的const跟之前c语言中是一样的,代表不改变,因为prhs是指向输入参数的指针,mxArray这个数据类型是在头文件mex.h中定义的,in fact ,在matlab中大多数数据都是以这种类型存在。 还是拿之前的栗子来解释: - ) E9 p# x! c, q. U7 r; u
9 `: Z6 A- ^0 k- `
#include "mex.h" // 使用MEX文件必须包含的头文件
- v/ J1 J6 ^2 l4 z
, Q' K3 m. h. p; S( ? x
+ |, ?# N2 G. a1 X/ F: Z& Z
3 M1 u: ?8 \0 L6 F# q, t& w// MEX文件接口函数
4 S# l L# i# @3 h# W% N
8 R( o2 Y( v& a( }1 w
5 y% |& B! Z1 C+ q; g6 `: S5 \2 @: B5 d
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])$ W: w4 U' i' `; g, B2 p
. L( E. F+ q1 d
- N5 n5 \+ i, _ m6 q+ t6 M, R. h6 b
7 H" x2 n; X+ k* O. H% E( o0 {1 ~
{3 R' M5 V! S$ n2 c+ H/ q. W
g* o0 I8 A/ ?: }3 ?$ ` K2 |+ {0 r
# ~3 l8 l/ {2 Z: b* Y, ? c4 s: l. k2 i1 N t& Y/ m
double *z;, [% Y1 Z; w, j0 W& S- ~
( y7 G$ t1 ^; c7 \
7 i* i, M9 r+ \# C6 E* o) A% M* D' ]. a; b k0 m1 x
double x, y;' E+ x) g( e, z: i# s
1 `! ?# t0 V6 s! ]
- # e1 h8 t8 v' z* {6 ?9 i
$ d" E5 ~9 s/ I7 q) [: v; H0 } plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);1 Q. ?3 c7 v- Z8 m& l
+ q. } V; h& a& k - * _5 N7 ~2 V9 S/ O! Y) J% w5 E# H
# \, b$ W, ^; z& w" F" u
z = mxGetPr(plhs[0]);7 Y H7 N' }0 D; m2 p l" c; K+ c
4 l* j0 I( j c
- . `' Z0 |3 z& k0 [; Y1 S
: V; \4 J) @7 J/ O
x = *(mxGetPr(prhs[0]));
7 ]+ P7 \ W' S/ r! D5 l
2 K2 _' v. Q* L. t8 Y h: P0 G - 4 c1 [; r( n. s2 F
' E' \. X) X. g* m4 U y = *(mxGetPr(prhs[1]));
* K% U/ J; @$ z K) E) j. H/ i1 i3 x, Z. g- }' C( k
- / C& C- h& P9 N# v
6 \) N/ i/ { e. O4 i! {+ |* ~8 G0 Z
*z=x+y;
6 z* m7 l/ ~# B" U5 R' Q# N
% r5 J, B/ v1 ` - 6 c9 I/ o( a# r Q* N2 C
4 r: @! a6 P( O* z! O; d. D( d' m
}, Z, F8 D$ ?: m5 r7 q: N- Z2 ~7 V n
3 |5 q! c; I, \$ C4 l4 a; N
# R6 ?: T6 j: x t& v0 G0 W# r9 P
在这个栗子中,输入参数是x,y,输入参数是z(虽然没有返回) plhs和prhs其实都是指针数组 prhs[0]代表指向第一个输入数据的指针,mxGetPr代表获得这个指针,后面再加个*就是c语言里面再正常不过的按指针取内容了 所以就这样x,y通过下面这两句传入到了mexFunction中 - & o3 z8 E t: }$ I) r, H
% E# ~: T2 r5 D# E
x = *(mxGetPr(prhs[0]));& j+ g M' j! N+ ~+ m
7 K* }7 v& q1 G
+ y, @) D1 u/ j% j
$ ~8 z3 J" r* R. Ly = *(mxGetPr(prhs[1]));- u2 }" l. n5 s: S# E8 z
4 K& u5 z K x
g! b) N( S, j; U. _; f" P" m
mxGetPr函数的功能是从指向mxArray类型数据的指针prhs[0]、prhs[1]中获得了指向double类型的指针 另一个函数是mxGetScalar,Scalar即标量,功能是把通过prhs传递进来的mxArray类型数据的指针所指向的数据(标量)赋给c程序里的变量。前面的mxGetPr是传递矢量的,否则矩阵或者向量就没有办法传进来 这里由于传入的每个参数是单个值,于是也可以用mxGetScalar改写:
$ n. Z1 r+ u& t9 C
9 w3 Y- E$ ]+ x. b9 p; ?. `#include "mex.h" // 使用MEX文件必须包含的头文件
* W5 ~6 `3 K' |8 O" c5 {! X v! I7 J0 [
2 v5 F- [, z# T% _3 X; U+ s# i4 P; J" s p; _! Z8 T, _8 M
// MEX文件接口函数
7 n+ Q: X, c, _8 U. [0 i& c+ O* ?" z4 E( ~! N' Q
- / D7 R0 {- X" o/ J; Q! L$ f
% M3 ]3 j9 ?0 K; {( a2 r# w
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])" n0 L. a1 k2 a
* t/ g# A/ P) T% p - : | ?+ [8 m u0 d. j
, k" j0 b2 |( x6 C3 V0 ^{
- E1 ` a+ }- b; B+ k' v! f4 }6 Q( X' [ ]8 H0 C' s1 t1 {
& n5 y6 p9 k; {% [/ G; C# ]4 ]4 F& Z/ d% V" j' K
double *z;* p/ M' o* ~3 Q
+ ^$ _; ?3 k- z* E; U
- ( a6 g# s* c) \4 [6 ]0 F
/ h* @4 L7 k G' R) {" U
double x, y;
5 q/ |& e9 ~# x0 t4 E' k" K% s+ q& ?8 _, d# v
5 v5 Z1 D0 S R3 v' f( m( w3 j5 Z
! K4 j" w2 |$ q' t8 d plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);% m2 N: V$ `: q2 z8 N) {: W4 ?
$ f' l" t m$ k j
- 9 _, O4 m2 d1 t+ o9 k
( ]" N8 N7 c! G" Y! r8 R
z = mxGetPr(plhs[0]);
7 Z, h% z% v6 u' R# \1 Z; h8 n4 P6 V! @- m/ X: ~8 h2 y
- 7 I5 X1 n! @) m; V' r* H
' m+ Z! r5 c6 I0 D) c7 P
x = mxGetScalar(prhs[0]));
: H/ p) `0 h. E/ Z7 F9 x7 b N/ R1 I) V4 {* h/ b6 C
# x- a5 K( O7 F3 R8 k
' k3 s8 G9 E9 k4 w y = mxGetScalar(prhs[1]));
2 q) v0 Z+ N* e8 j w, `( N
! f6 f1 ]1 l. @7 g6 c- W
+ c# H& O2 F# X8 n% ]( S* p: [7 c- f) j9 S+ d5 \
*z=x+y;8 ~$ h Q2 \, F
' Y% [, J- q) y5 j
$ k9 a, q8 z- d4 x7 L% E3 w6 o& r2 W5 @ L+ C& L3 g
}, o w! [0 _5 X) }/ t" a) j
8 l' K1 M/ m5 G. n& I
8 Y3 V m1 A2 i+ U @: l
(注意:输出参数因为需要通过指针传出,因此这里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]
举个栗子:
% F& V A" _, e9 ^& a2 P/ s. Q5 F/ u% u" @
#include "mex.h"6 n7 `+ K, z6 w* ]* i: K- J" e9 _
8 T& U/ u/ V" y. b- 7 Y( i' w3 v/ I) Y9 b, i4 ~
. T9 A; x' O+ w7 _2 v, k
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]){
3 n+ J$ m$ [5 [7 h% A* _4 U$ _1 S- h/ n2 V6 `5 U5 s7 \
- : r. x% ~6 C% v* O9 J! Q/ G
' L$ M$ G" q8 e# D2 ?4 p% b1 Z
E2 D: _! b; x( |, s' `4 f% c/ R* ]$ G! }2 I
; g. j0 p8 \- ^$ |8 d9 S% c5 J
1 l- R) L. @ D/ q' Bdouble *data;
$ E" G: T0 ^2 X$ m- o6 O* F: m& T
; m O. ^1 r+ R3 {6 g0 V- - }- ~/ V& o: t" V2 S: c
) k6 j; U3 ?- A- G8 X3 p( wint M,N;; `! U: ]$ a- h2 v# V
: M% k' W2 G5 O, ?
4 ?) p; ?+ f, z0 v$ U
+ v5 e- [5 w% t, `( zint i,j;
4 o [- U3 t, Y3 N$ P6 U
: @5 J, [8 ^& k% H; M0 W9 m" x
) B$ [" B; }# |6 y
6 s& p" m6 B1 ^$ m2 jdata=mxGetPr(prhs[0]); //获得指向矩阵的指针
' }2 \- a; V$ a* _8 x' K$ ^# P" t$ E6 t, `/ i3 c
9 [3 b% p6 ?- n9 Y% |! |% G4 y2 I9 U7 ~7 Z
M=mxGetM(prhs[0]); //获得矩阵的行数
; W0 y" J' X* D$ e! D
$ _) v7 u3 s: b' Z# q. ~- # N, E1 z, ~2 u1 X
% ?6 y) `1 @9 aN=mxGetN(prhs[0]); //获得矩阵的列数
- D9 o. F3 j6 D$ I' {, F' q
; Z% w9 A9 H- { I4 t
/ \8 x( e7 T' I6 ?3 ~% E R+ A" }) H7 T/ r
for(i=0;i<M;i++)
# N1 o% ~8 O8 i; W' O6 r# T
9 |. h. v1 X5 g/ h
7 u0 ~& t0 c7 Y9 G3 `
; q. J8 L/ Q8 m" S{
% k. X. K2 Z7 N) j
& i) h* ]4 @8 R# g8 P2 y
8 R9 b, Y( z9 G& F/ `+ L& L2 D7 | ^% o! g5 }5 |# M" ]" C$ C
for(j=0;j<N;j++)5 Q; S2 w: w1 R+ V8 @6 _3 i- r
2 n' @) Y0 u+ F1 e0 `# }- 6 M2 m1 _0 H7 b! X( x
. ^6 a+ V2 @2 x4 L( W# ~ mexPrintf("%4.3f ",data[j*M+i]);
6 \' D+ I& Y1 o1 L" Y0 p+ f5 p: Z1 y
3 J3 f6 G$ P: `- |4 b8 M
' P' {" i. J+ G- [; U5 A9 J mexPrintf("\n");
. F$ v+ s1 C+ J) n+ w" i" o1 W6 G3 y' ?% |' T
3 s$ O! S$ l+ d' D8 S% ?; l7 M V
9 a, Z* L" Y) s- V- j* ~5 k0 j; E}- L; X& b6 |! T' J& O
) F1 O; }8 l& l3 ?9 U; j; m8 N- 0 l4 S+ g# e* a3 a/ a
( x+ T# j& X. |; C/ p }2 I2 E' w. d$ y# O
# O; O+ m# N4 {+ T' R
% [" C, O6 m5 Q2 F7 |/ u5 m
假如是一个形如[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
8 _6 E1 t# o- K
6 P& f% G' n0 W. N
- Z1 c) A( l" E- Q& ?6 d
* t+ ]6 W$ L/ S# {: E/ Q- & |6 J+ K4 k5 Z: @9 o7 C% ?. C
- T2 i+ l4 ~5 m
double *z;
% l0 N, X6 S) Z2 h7 @' T* N
w) @4 Z& r9 H* a- S* X: O ]
/ t+ C3 q" G8 H, M
2 G" g p, i8 u& Q- X: ]+ d plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);% b+ I M4 o" e+ e# \
8 g/ U1 R" z2 m; H2 l1 i- * Z3 M+ f! C9 l5 T
4 L! C7 a5 G& y; m4 d3 W z = mxGetPr(plhs[0]);
/ d+ f$ A6 W1 q/ a: p
& z: ?& x. z* x - : f( |; j/ X1 `/ o& `
; s: b: p+ F% J; O8 @* n0 o' @ x = *(mxGetPr(prhs[0]));& m7 I7 ] f7 f8 n
' b$ j; d' {& ~
- & T' F6 _/ a0 k+ u- X: {. V
5 Y) n L0 U" C& f7 j
y = *(mxGetPr(prhs[1]));) `( z& g( g. d& Z, |
5 z# n5 n( H5 j( \- S5 O
8 L1 w( V+ x3 K% a+ V1 u1 C7 }
2 |- g% B6 k7 C% c" M. C *z=x+y;
& V( c: D& v: t8 }& X& u& T# ^' Z2 t; R
" l Q$ w4 [8 C) K" y- G
当然,matlab里使用到的并不只是double类型这一种矩阵,还有字符串类型,结构类型矩阵等,并也提供了对应的处理函数。 & I$ W# x; J( _. ^% e" e. |
|