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部分
# ?8 u9 i1 R4 i8 V1 d- v
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[])举个栗子:
2 ]9 X* K( S3 c; i; m$ R; v4 R
#include "mex.h" // 使用MEX文件必须包含的头文件 F# [8 T. t( f' Z* ^- c- R9 P
+ ^. Q9 J5 p6 a$ |9 M
% h: Z/ T2 L$ ]1 N. V" u( s T2 y4 X
0 X& e2 m. L1 r6 v, b6 ^// 执行具体工作的C函数
6 H: W3 t8 j! ~5 _* ~2 Q, d2 z: ]/ G5 Q/ P. A2 B5 G( [
- / |, m0 ?- f [
" \3 K2 j3 i7 s! f
double add(double x, double y)/ X0 ?+ g# w/ g. _0 n
- k# D( j7 O ~ - " u' J" c8 }" t( ` ?
6 ~* Y& Y9 W5 b2 V$ s: @
{2 Z6 ~& ? ?$ o( U# l. ^6 O5 w, h
2 O2 f/ E3 ~6 d7 j
0 M2 ]4 F" Y Q9 x* P. H5 \
% l1 j$ x8 K- C$ n+ A ]2 W return x + y;
: a" j8 z; v! j5 M! K
5 c; L' R# E6 o4 S; y- 3 x. F" R0 T" J& K6 @2 f
( y& w/ n; S7 N6 W+ q7 {
}- C$ U% X0 `" |& a8 \* c$ |) [
) B( A3 K6 q0 ^; u) q9 O
( n" p# x a* n$ f) r5 W) j: G# `# \$ K" s" h: u; w9 I
// MEX文件接口函数4 C% ^6 B: Q7 Q' M/ {0 f+ j0 Z
1 o* R b. z' `9 ?+ V) t- j8 n" Y0 f
- 1 E2 O& a/ `9 N. o
9 n; @7 C0 _ Z
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[]) o. T$ @: d5 k; S0 u2 h4 @+ p0 A
$ ]7 }% i6 P; r6 D4 {; I2 t - ) Y. T$ Z4 S$ ]! Z8 G X2 l g
/ v% c% P* W6 I `5 y" s" F1 C$ E( g
{
8 k/ \0 k& n5 n
8 M" v0 z) d8 i5 @ - + m( C0 C2 n6 |
0 ^$ c, q3 F! N! x1 c double *z;0 {6 o9 ^5 w' j$ s) E
5 o8 p& g0 R2 y Y
/ m; v) [. P; F+ A$ P0 p# P( m7 {( E5 K
double x, y;
9 V# a, W4 s- @& ~( x. `) t0 i) j& |) m5 z/ ?
- 0 [/ s* g. ^4 ^/ d3 }1 G8 _
4 a! k! n! A3 S1 m plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
# }. `! N6 Z& C+ j% \! ]3 b2 D1 f7 V2 R( H
- * H9 K4 g1 h1 `+ v. A0 S; ~) Y
. Z$ U5 l7 o5 `4 Y4 W
z = mxGetPr(plhs[0]);
9 c+ w, _: m- d9 g7 e7 |4 Y0 U+ r+ R9 D
- 4 @9 J8 {+ b" R
- \/ }( x$ N6 @
x = *(mxGetPr(prhs[0]));, L. J9 W7 |8 P1 N0 [5 k, `
. v& C x" p9 ^# w! j
* i2 F- W6 O; P' h! d! i
2 P# O8 ]- ^+ a& T y = *(mxGetPr(prhs[1]));* f! D, g: z+ Y* i
+ |) o/ Z" L" C* H
- 0 ?7 {# N6 D$ o
3 N; \! V, l( H) M* s' X, v
*z = add(x, y);
9 ^( i; Q6 }& ^$ O( I5 q9 W1 O% E0 Y$ _( o+ v# k
- 3 C" G& H: ?% O" s' T
0 s2 w. v0 F) h9 q
}' f7 x- D2 q- O6 M6 z* @! v, d0 b
. W$ ?8 W7 B' ]! y9 ?' m7 l/ m* R8 X. u- b" R
也可以写成: - $ E; {( l) I8 R1 r% q
% a4 E& U6 [) Z' w* T, z#include "mex.h" // 使用MEX文件必须包含的头文件
- M/ B; r% \- `( l
1 A! ]$ _/ q/ o; h$ m
# H: G9 t4 x2 Q/ r) G1 ~
' Z% z, P' x& p$ J7 n8 E+ D// MEX文件接口函数6 Q& c* l. B# \0 C3 o
3 r) f+ R, s5 s: @* R. @
- 5 f. w' O: Z. [6 ^! D9 A3 S
/ Z9 g: l. H5 V8 F: ~5 {/ Z1 Mvoid mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
7 y0 f) ^: Q# P5 x+ ^1 F l9 |7 f- \4 V. k2 u4 ?
- & B, o4 _" N4 m, z y
8 E9 u0 N- o' [ ~
{/ w4 ]: C/ p6 V' V, [( E
/ V/ ^& [$ T% \& ?4 K d+ o
, s3 r7 ~- R' { M# g& Q) j& z$ i) E; k7 o2 J
double *z;
6 {8 {; g4 h/ I" C! |
' Q* D' v- ~, S- d
- t6 H$ N' X. h- R/ D6 z# N7 U+ Q: @2 t2 O) \$ _, n* ~& U
double x, y;
# f# D" a+ k2 M1 k6 }
! Y( ?9 x9 _* y- , Y; \! O5 N9 A' P" }& r7 M
/ F$ V. M% I3 R& O3 c5 s/ ~$ n* h
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
: i0 E% ~+ A4 z' Q' T
- E4 d: g6 {$ V - 0 J9 R6 ]* k- R
+ e/ }6 U/ O; S0 w" K
z = mxGetPr(plhs[0]);
" `( o7 ? M- m$ T6 E4 E
/ M; p; w: K0 R/ j2 w
1 h4 G( v! i+ i2 v% J# Z) r8 C( r: [* H. u( R
! v+ `9 q) m: G& _ C V x = *(mxGetPr(prhs[0]));7 |9 E. e0 w5 G. C1 R
! M8 d# B1 P5 S0 r% S, D( |- # D" G4 z9 W( U
9 [; ~. W/ }! i9 g1 ?+ Y. K
y = *(mxGetPr(prhs[1]));
' Y$ ^0 R: @% d* N- @" M+ N& Q' W2 ], T y* Q0 Y5 K5 d
8 u3 U) ?! Q+ V1 M7 w7 R# t: `3 A8 T5 |: ]) n2 q
*z=x+y;
0 n, A* f& r! [" }# B. W9 o
/ t: K H7 C# D- ! S5 a2 l. g' O1 J
8 r* ?3 f3 I. n |% d; v
}5 h% J1 A! Q) e3 h
- V# J/ o* t; L- [3 o& f# C4 z! N
也就是说执行具体功能可以通过编写一个独立的c语言函数在接口函数里调用,也可以直接写在mexFunction接口函数里 我个人推荐将具体功能独立出来,这样一来: c语言函数负责执行具体功能 mexFunction接口函数负责数据的输入输出(将matlab数据输入c语言环境,运算后再输出回matlab)形式组织 再调用c语言函数
分工明确。 于是关键问题就是如何编写mexFunction接口函数了 void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])下面解释一下mexFunction各个参数的含义(这里看到是void,即无返回值,因为传值是通过指针数组plhs[]传出的) 参数 意义 英文全称 类型 1 [, v4 ~3 g# N5 R5 k5 P7 k8 q! d: q
nlhs左边输出参数的数目number of left-hand side整型(int)0 i L! \, f* n5 j8 [2 J5 N9 p
plhs指向输出参数的指针pointer of left-hand side指针数组
2 n( j( Q" m) w/ Hnrhs右边输入参数的数目number of right-hand side整型(int)- H2 n$ a6 f) v, }7 Y2 L5 z1 {$ p. ^
prhs指向输入参数的指针pointer of right-hand side指针数组
" ?% x4 E% d0 i/ V, I0 O& [% x# [第一个参数nlhs是输出参数的数目,第二个参数plhs是指向输出参数的指针 第三个参数nrhs是输入参数的数目,第四个参数prhs是指向输入参数的指针 其中plhs和prhs类型都是指向mxArray类型数据的指针,prhs这里还加了个const,这里的const跟之前c语言中是一样的,代表不改变,因为prhs是指向输入参数的指针,mxArray这个数据类型是在头文件mex.h中定义的,in fact ,在matlab中大多数数据都是以这种类型存在。 还是拿之前的栗子来解释: - ) K) r5 `& _. G4 v' Q
" g7 O3 j! q% @) X7 z' N#include "mex.h" // 使用MEX文件必须包含的头文件) N$ P# z p+ G
* o, j2 n$ ?) c; O4 b8 [9 l - & W1 W p$ Z' Y# D; p; u% Y: a) E
- O; \6 F/ S6 j) n& X8 C! _
// MEX文件接口函数2 D# ~% P* W/ ^
1 m" b5 B! \* d6 j6 }' u
7 c+ Q& t" r3 r. h$ f4 n+ p8 `4 D7 i% o0 e% m2 n
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
# U% ]/ O0 h J9 Y9 H& }
/ K7 p/ ]/ X: @5 f% E' J9 `0 b
- l9 `+ H: ?/ B0 |" J2 `3 y5 g) u0 h+ D1 X( Z& y
{ E7 M5 Z- U: n: \/ ]1 H
" N8 `1 U# g4 d5 O* F9 e
- ' n% b3 T: y: c6 ~ R! O
" e. o2 @. d% E2 [8 C8 }% |7 c' K double *z;. T! l. K8 i0 U6 a8 j1 I) _
# {8 ~! K: q( b, S) Y
: W1 R. i$ J ^; p; P% l5 H2 U0 {8 f& @. H; f
double x, y;+ S) d! J9 f' F( m
% m5 b, Z* d1 @* B
' \; o) b# ^3 M& H; n% O
5 g9 [+ ]" _2 E; C: K5 l/ h plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
* x( N$ ]& z3 S. x
1 z3 V0 o4 z3 w- Q- : J, i. P/ I6 C Y6 O8 K6 t
0 c8 e" {! T5 S1 x z = mxGetPr(plhs[0]);3 d G, L! w6 {9 e" M8 o
- _' U& H9 `' N ]
, v/ q: r+ X! `3 o6 ~
/ d2 X( `. s3 F% k7 H2 U x = *(mxGetPr(prhs[0]));
% R1 }# V3 I1 `4 s6 B0 a
2 D+ x3 ]$ H; M5 N: I) q- 1 u) ?3 K3 g2 E( Z: {9 t
: [8 G: L* d* k/ A" q1 N! K* k# C y = *(mxGetPr(prhs[1]));9 k7 b% t% w k( D9 Y
8 y/ Q' i( s& O- y# ~5 Z0 U0 E l# X4 n6 A
~2 \* v6 D, v* l z7 O$ x! S
*z=x+y;
1 D" u& Q% T @* c
, m' q6 _, D1 N- & v# a" x ?/ \ J
5 @+ ?: Z) u- p0 J# s}
; M6 G9 w' s0 {( m+ {) R; |9 c: O. {5 a+ E
6 ] x& U4 ]5 N/ q# _
在这个栗子中,输入参数是x,y,输入参数是z(虽然没有返回) plhs和prhs其实都是指针数组 prhs[0]代表指向第一个输入数据的指针,mxGetPr代表获得这个指针,后面再加个*就是c语言里面再正常不过的按指针取内容了 所以就这样x,y通过下面这两句传入到了mexFunction中
5 e1 E$ J/ }# v" p4 Q
5 \- Y7 U K- y( Y5 f8 Bx = *(mxGetPr(prhs[0]));
0 }$ V% M) _+ m( n- X; m# y# J% E. ^3 Q
- 4 }' k* W( y8 t j; i
: K- P' E# m# H+ W: N5 H1 v+ y8 qy = *(mxGetPr(prhs[1]));
3 E7 @; U& g& m% G$ U8 G8 |1 t8 F! E- U
) j+ \* g6 x. M' e; n' Z" o
mxGetPr函数的功能是从指向mxArray类型数据的指针prhs[0]、prhs[1]中获得了指向double类型的指针 另一个函数是mxGetScalar,Scalar即标量,功能是把通过prhs传递进来的mxArray类型数据的指针所指向的数据(标量)赋给c程序里的变量。前面的mxGetPr是传递矢量的,否则矩阵或者向量就没有办法传进来 这里由于传入的每个参数是单个值,于是也可以用mxGetScalar改写:
# e7 R. ^. B9 U6 U, B% E( A- o3 g% z! [( |
#include "mex.h" // 使用MEX文件必须包含的头文件 o( R' t& Q u& B
, Z) u9 K5 G M H2 q; Z
) P; S, U; Q9 d! d
4 I% B- @0 K2 I/ n// MEX文件接口函数; I* E8 _- [' G+ o! ~
/ p% w3 |- G0 O6 j j1 z
9 S: ~2 a8 h2 c0 Q+ [7 B- k4 D* g# G. x" W* I9 m& l: E
void mexFunction(int nlhs,mxArray *plhs[], int nrhs,const mxArray *prhs[])
4 ]( o3 Y0 Z7 u: O
+ E- H! w) S0 g; |" ~
5 P& d" W8 Z! Q
9 A* e, v/ M" F5 C7 U- ~{ C% x) g$ v- d( x
/ S2 R3 t8 Z: e( ]
- " g- I! P+ C& C& p
( e7 E# D4 z( ^0 x0 p) g6 m+ j double *z;
+ a; ~5 {) b7 l; t" C
$ V# `- S$ C. u - : I2 E/ u8 n9 X2 `9 N2 y7 _5 M7 I- {
! G' p1 [6 Y' x3 |2 D/ }( _( w double x, y;
, E7 w+ U1 e7 i* n; R( w* X% |3 V6 l& k q2 g$ ]
- ' _* B$ Z0 S- ^# h+ j
" F3 X1 F7 j* X1 ]4 H0 c u
plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);2 m8 @: V$ e" b& k4 y# B
* q; ?+ h% u* i* C
- ' {# _5 q# y6 H& N
+ u4 a# x1 q( M z = mxGetPr(plhs[0]);
H% t' _8 l+ }7 H7 d
7 j) _ G5 _6 |
3 d/ {$ g; l$ j# X$ F( e: _ B4 A& g: Z5 ~) U8 C; C/ Z
x = mxGetScalar(prhs[0]));
9 P& @' x! N6 V0 S; }: F" }" X3 Z8 V
- 4 T, H2 b+ ]0 }4 J7 M. b' T5 }
% }, ?+ X# M7 `# `
y = mxGetScalar(prhs[1]));. a! _3 P5 b8 j$ E: C* ], f6 ?* ^
, {1 Y4 l) W! e/ X
5 Z o: I9 }) G* C: B; W0 [/ X. O6 c* B$ @
*z=x+y;4 _1 X* S. H3 I+ Z8 b' i
; Z. X3 X+ w$ s! }
, F1 Y, k" X2 a. ?7 y
* g- Z8 `1 {1 s" Y0 A}) {( `9 T- h! `' m7 Y( o
" D3 w/ n7 f* u' E5 T7 V9 I+ R! c1 j% X( z4 p# G
(注意:输出参数因为需要通过指针传出,因此这里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]
举个栗子: - - `9 O6 {. n" k$ d1 ^( v1 ]
- z% s! o5 t5 T0 }. K0 j Z! G/ y# N#include "mex.h": I! ~! ]- n: f( |$ f
% K5 q: {- J9 }1 @: D O9 R% E
9 C3 _: l4 D& @0 l3 d) a4 z0 o0 U8 Y
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]){5 l. a7 n; b2 n- ?9 y) Z
. M0 v6 J1 u1 r( V3 \
: x0 q- u i# z# } q# v9 {; \- M" E" R0 F' B
* Q5 F* v: }, w) Y: U$ M# L: L/ C
" S4 u- P* T9 r- P0 V
- 3 b0 M A! J0 d
! Y. j/ ]8 T# T0 D& q9 H1 o
double *data;, l/ {5 x, V- R
# y& M2 i% X8 g; O$ q
- \! | a3 X H# `6 @
" \$ K2 r* F7 {" Y7 aint M,N;
: e+ i, L, M9 ]8 f
% ~% r4 s w$ z6 z - c/ \+ ~6 c4 P; a7 M7 G
; L( c/ J. Z/ Y" [5 X8 C
int i,j;' ^5 Z) j6 `( \" d
8 P+ A9 h' l t
* u2 b# O' F; V6 P7 j1 g% \* D) Q- [) _* B( R4 m8 I* U/ h" T3 B
data=mxGetPr(prhs[0]); //获得指向矩阵的指针
; ~6 ? x' m+ o+ |$ x; h3 R
" ]. K$ _$ d7 h/ Y _" C% \- 0 @8 M; C6 u7 w( v; W1 E
/ T, P+ r6 _- d) S! D. u1 hM=mxGetM(prhs[0]); //获得矩阵的行数4 [% c% A+ k0 q4 e+ V' z( p
# x6 R0 f8 C d' G& ~/ I l
- 2 q% g7 c7 @" T
1 A+ O2 T! M$ H$ o4 t' K( O
N=mxGetN(prhs[0]); //获得矩阵的列数
7 b2 l, a( K2 C. g' u& o, }* Z% b$ S0 W [0 X& `& a
: P0 Q' `# `) W
7 e9 W3 W, a. z' C5 P2 Gfor(i=0;i<M;i++)
3 v/ ~- p2 u1 g' {7 {% h9 B' }% ^3 q' j( q9 N0 h, j# k
! |: T7 @ } _% z. O3 S: ]4 F% S1 B7 x& R+ h& X/ a8 G1 }- \8 t
{% ^5 s5 x [# Q* ?( b! x
/ {( d1 i& a3 @9 U
/ `5 u) [+ B. Y6 m+ R+ H8 k
* k! R( Q7 i' j% r for(j=0;j<N;j++)
- P( l& D s5 w& w0 I* V! L7 s0 a2 Y3 B7 k% L) y: n2 ? K) m) y
% T1 Z4 a, H- p( z8 {- a! ?1 W( k/ N% F7 u- \" J& d
mexPrintf("%4.3f ",data[j*M+i]);" r( @3 f* K/ y0 ^
# d3 @; b# { a' ~- % h% T! _- `8 ]+ P( ^$ h& f. t! R2 ?
2 ?; z7 ]- V. l& d3 v7 Y w" d$ F3 x
mexPrintf("\n"); D6 S3 b; H4 O G5 u3 A
3 A# b- N8 u" ]6 u' o6 l6 e
- " f4 D* Y( S) ~2 W6 n( r* H
8 h6 J4 K$ w1 ?3 q: u# D0 E8 ?}/ J# W" F1 c; A! W* @) g1 q8 ^
1 Q0 i" S6 j( Y, S - ( @" ~& q& m1 _7 f
% ^, ~" G1 B0 ]4 G5 i
}8 @" x" b" W9 p. m+ g, c
, m( X' g' [ s8 M1 E! Y3 h9 P
; a. Z; M" e# n2 K' y( Q3 R7 P, F2 q6 _
假如是一个形如[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
6 m3 K" m5 r; P- q; \2 c
% J& ^/ X- s2 U5 y8 m
' R$ ? e) R; x3 a* d f* Q" L+ J- e% t3 F X
& x) `; t* _5 p3 o+ e: H. i( `; k3 |; |( l3 l$ X
double *z; f+ F* \. V) h- r* s( l
$ ] F! [" ]& l2 W- 4 H7 J! R! \* m7 s
3 R% e, J! X2 H( m% p, Y7 e2 o( ] plhs[0] = mxCreateDoubleMatrix(1, 1, mxREAL);
" M! n/ V2 d$ o% i0 g; f" _! r) z; h8 E
2 m7 q4 Z# C! W$ L4 x+ {. U9 ~1 t
z = mxGetPr(plhs[0]);
4 i" _9 \+ S1 D# y7 H! r" Y X8 }6 d# r5 S* _
* P- _& @& A: m( B8 `1 w- A, G9 `/ I/ _# r$ `! k
x = *(mxGetPr(prhs[0]));
( A v) z/ w! C4 _2 w7 {0 x
! L" O5 A I. u# O& z: n- 3 ^3 B2 O6 N7 Y8 U/ U$ V
/ x; [2 ~+ |# r) p" d) p
y = *(mxGetPr(prhs[1]));
; ]5 C6 y2 H1 y" @
1 q5 K2 i: w/ E+ x Q - . `, t. f8 t4 \% l0 m
" K( f i( M) z- \, |4 [5 R *z=x+y;
! M3 r( z+ C' s' }( u3 w
, F8 y( U/ {4 a8 S* S+ Z9 z, q, c' h2 d! M1 L4 A8 Z* `
当然,matlab里使用到的并不只是double类型这一种矩阵,还有字符串类型,结构类型矩阵等,并也提供了对应的处理函数。
. q1 p3 R' ~ p4 e2 o/ A2 h3 N |