News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests

Main Menu

New right trim algo.

Started by hutch--, June 13, 2014, 01:14:42 AM

Previous topic - Next topic

hutch--

This algo does a one pass from left to right, determines the last valid character if there is one then terminates the string after that last character. If it is an empty string or a string full of spaces it writes the terminator at the beginning of the buffer so it is a zero length string.

The idea was to avoid alternative techniques where you had to determine the length then back scan to find the first non blank character. My guess is a one pass version, even though the instruction count per iteration is high is still faster than doing the same task in 2 passes.


IF 0  ; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
                      Build this template with "CONSOLE ASSEMBLE AND LINK"
ENDIF ; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

    include \masm32\include\masm32rt.inc

    rtrim2 PROTO :DWORD

    .data
      item1 db "    this is a test    ",0
      item2 db "                      ",0
      item3 db 0

    .code

start:
   
; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

    call main
    inkey
    exit

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

main proc

    LOCAL txt   :DWORD

    mov txt, rv(rtrim2,ADDR item1)      ; string with characters
    print txt,13,10
    print ustr$(len(txt)),13,10

    mov txt, rv(rtrim2,ADDR item2)      ; string with spaces only
    print txt,13,10
    print ustr$(len(txt)),13,10

    mov txt, rv(rtrim2,ADDR item3)      ; empty string, must be valid address
    print txt,13,10
    print ustr$(len(txt)),13,10

    ret

main endp

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

OPTION PROLOGUE:NONE
OPTION EPILOGUE:NONE

rtrim2 proc ptxt:DWORD

  ; ------------------------------------------------
  ; one pass left to right to determine last valid
  ; character then terminate it after that character
  ; ------------------------------------------------

    mov edx, [esp+4]                ; load address into EDX
    mov ecx, edx                    ; store that address in ECX
    sub edx, 1

  lpst:
    add edx, 1
    movzx eax, BYTE PTR [edx]       ; zero extend byte into EAX
    test eax, eax                   ; test for zero terminator
    jz lpout                        ; exit loop on zero
    cmp eax, 32                     ; test if space character or lower
    jbe lpst                        ; jump back if below or equal
    mov ecx, edx                    ; store updated last character location in ECX
    jmp lpst                        ; jump back to loop start

  lpout:
    cmp ecx, [esp+4]                ; if ECX has not been modified (empty string)
    je nxt                          ; jump to NXT label
    add ecx, 1                      ; add 1 to ECX to write terminator past last character
  nxt:
    mov BYTE PTR [ecx], 0           ; write the terminator

    mov eax, [esp+4]                ; put original stack address in EAX as return address
    ret 4                           ; BYE !

rtrim2 endp

OPTION PROLOGUE:PrologueDef
OPTION EPILOGUE:EpilogueDef

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

end start

jj2007

Timings:

Intel(R) Celeron(R) M CPU        420  @ 1.60GHz (SSE3)
77434   cycles for 100 * rtrim2
76024   cycles for 100 * rtrim$
34988   cycles for 100 * Rtrim$

76767   cycles for 100 * trim2
76382   cycles for 100 * rtrim$
35256   cycles for 100 * Rtrim$

76691   cycles for 100 * trim2
76024   cycles for 100 * rtrim$
34993   cycles for 100 * Rtrim$

Gunther

Results:

Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz (SSE4)

43270   cycles for 100 * trim2
26528   cycles for 100 * rtrim$
9904    cycles for 100 * Rtrim$

42794   cycles for 100 * trim2
26361   cycles for 100 * rtrim$
9810    cycles for 100 * Rtrim$

43108   cycles for 100 * trim2
26416   cycles for 100 * rtrim$
9834    cycles for 100 * Rtrim$

8       bytes for trim2
15      bytes for rtrim$
10      bytes for Rtrim$

--- ok ---


Gunther
You have to know the facts before you can distort them.

dedndave

prescott w/htt
Intel(R) Pentium(R) 4 CPU 3.00GHz (SSE3)

93453   cycles for 100 * trim2
91914   cycles for 100 * rtrim$
57921   cycles for 100 * Rtrim$

93506   cycles for 100 * trim2
92673   cycles for 100 * rtrim$
57474   cycles for 100 * Rtrim$

93606   cycles for 100 * trim2
92791   cycles for 100 * rtrim$
57808   cycles for 100 * Rtrim$

hutch--

JJ,

Do you have a copy of your rtrim algo as I cannot build it with the normal masm32 system.

jj2007

Quote from: hutch-- on June 13, 2014, 03:34:08 AM
Do you have a copy of your rtrim algo as I cannot build it with the normal masm32 system.

Not sure if that is helpful, but here it is. I guess the "speed secret" is the call to MbStrLen aka Len()

MbLMR proc uses esi edi ebx ecx edx srcLMR:DWORD, ctLMR:DWORD, stposLMR:DWORD      ; 192 bytes (11/2013)
  if 0
      mov eax, ctLMR
      bswap eax
      .if al==128
            shl ctLMR, 1
      .endif
else
      .if byte ptr ctLMR+3==128
            shl ctLMR, 1
      .endif
endif
  mov esi, srcLMR                  ; if not 0, esi is a pointer to the source string
  .if esi==127
      push 0
      call MbGetSlotPointer
      mov esi, [edx]
      mov eax, [edx+4]
      test eax, eax
      .if Sign?
            neg eax
      .endif
  .elseif esi<127                              ; PreAddLen
      mov eax, offset MbSlots
      .if !dword ptr [eax-4]
            call MbBufferInit
      .endif
      lea eax, [eax+8*esi-80]
      mov esi, [eax]
      mov eax, [eax+4]
  .else
      push esi
      .if al==128
            call MbStrLenW
           
shl eax, 1
      .else
            call MbStrLen
     
.endif
  .endif
  mov ebx, eax                  ; Len(esi)
  mov ecx, ctLMR                  ; no. of characters to be copied
  if 0
      test ecx, ecx
      je lmrRet
  else
      jecxz lmrRet                        ; nothing to copy, nullslot - nullpointer??
  endif
  mov edx, stposLMR            ; edx loaded with start pos: 1 for Left$, 0 for Null$, >0 for Mid$, <0 for Right$
  test edx, edx
  je lmrRetZero                  ; nothing to copy, nullslot - nullpointer??
  .if sdword ptr edx<0      ; Right$("Hallo", 3)      (traps also bad MID$("...", -1, n)
      cmp ecx, eax            ; ct>len()
      jg lmrRetFull            ; Greater (signed)
      add esi, eax            ; Len(src)
      .if sdword ptr ecx<0      ; negative ct allowed, returns full string
            mov ecx, eax
      .endif
      sub esi, ecx
      inc eax
  .elseif edx                        ; MID$ if stpos>1, otherwise Left$
      cmp edx, eax            ; start>len?
      jg lmrRetZero
      inc eax
      add esi, edx            ; MID$ start pos 1
      dec esi
      sub eax, edx
  .endif
  .if ecx>eax            ; unsigned, ecx might be negative
      mov ecx, eax                  ; ct > strlen: limit
  .endif
  test ecx, ecx
  jge @F
lmrRetFull:
  mov ecx, ebx            ; use full len
  jmp @F
lmrRetZero:
  xor ecx, ecx
@@:
lmrRet:
  push esi
  .if srcLMR==127
      sub [esp], esi                  ; clearing the stack is one byte shorter than .else construct
  .endif
  call MbGetSlotPointer      ; returns pointer to string in [edx]
  mov [edx], esi            ; pass the address
  mov [edx+4], ecx            ; and the len
  ret
MbLMR endp

hutch--

It looks like an interesting number but it is still unbuildable.

jj2007

Tough luck, Hutch - it's a bit too strongly integrated with the rest of the library. But if you are really keen on a fast algo, check the old forum for the fastest StrLen algo and use it. Or replace include \masm32\include\masm32rt.inc with ... MasmBasic.inc and use Len()  ;)

rtrimUltra:  ; fastest rtrim ever
    mov edx, [esp+4]                ; load address into EDX
    add Len(edx), edx
    .Repeat
      dec eax
    .Until byte ptr [eax]>32 || eax<=edx
    mov byte ptr [eax+1], 0
    xchg eax, edx
    retn 4


If that is still too MasmBasic, replace Len with len, and build it with masm32rt.inc - half as fast, but still a factor three faster than the original rtrim2.

BTW inserting the zero delimiter is considered an anti-social act, especially in read-only memory. MasmBasic Rtrim$() uses a softer technique 8)

hutch--

What I was looking for was a technique to benchmark and test different algos but it appears there is not one available. Sounds like you have a fast combination to perform this task but if I can't build it, I can 't test it.

jj2007

Quote from: hutch-- on June 14, 2014, 11:54:05 AM
What I was looking for was a technique to benchmark and test different algos but it appears there is not one available.
Attached. It's very easy to use and builds with Masm32 only if you wish so.

Intel(R) Celeron(R) M CPU        420  @ 1.60GHz (SSE3)

14953   cycles for 100 * rtrimUltra (Masm32 len)
73356   cycles for 100 * rtrim$

14960   cycles for 100 * rtrimUltra (Masm32 len)
72571   cycles for 100 * rtrim$


QuoteSounds like you have a fast combination to perform this task but if I can't build it, I can 't test it.
Sounds a bit like "the CRT seems to have some good algos but since I don't want to include msvcrt.lib, I can't test them" ;-)

sinsi

Hey jj, am I missing something?
In your original attachment you reuse somestring but hutch's rtrim2 trims it, leaving the other tests working on an already-trimmed string.

hutch--

There are a number of things I need to test that don't come in a pre-canned test bed, the attack rate (many short strings), the long read with a high count of end spaces as you sometimes get in log files to test any backward scanning techniques, whether the iteration count effects the timing etc etc etc ....

So far I have the original library version and the one I first posted here to test the newer one(s) against but as you seem to have a fast version, I was interested to see how it performed on multiple complex tests.

RE: read only files, if you need to trim a string from a file or source that is read only, is there any other way than to write it to a buffer while processing it ?

jj2007

Quote from: sinsi on June 14, 2014, 06:00:49 PM
Hey jj, am I missing something?
In your original attachment you reuse somestring but hutch's rtrim2 trims it, leaving the other tests working on an already-trimmed string.

Sinsi,
You are perfectly right. Ideally, one would have to re-create the string for each iteration, but how to time that??

I have inserted this...

    mov byte ptr [eax+1+40], 0

... to provide more realistic timings, i.e. rtrimUltra does not trim off the original. In round 2, the Masm32 original trims it, though.

Intel(R) Celeron(R) M CPU        420  @ 1.60GHz (SSE3)

31666   cycles for 100 * rtrimUltra (Masm32 len)
17806   cycles for 100 * rtrimUltra (MasmBasic Len)
73692   cycles for 100 * rtrim$

14948   cycles for 100 * rtrimUltra (Masm32 len)
4899    cycles for 100 * rtrimUltra (MasmBasic Len)
73104   cycles for 100 * rtrim$


Quote from: hutch-- on June 14, 2014, 06:11:48 PM
RE: read only files, if you need to trim a string from a file or source that is read only, is there any other way than to write it to a buffer while processing it ?

Valid point, of course, though I still would feel a bit uneasy doing that in a library routine. Maybe mention it in the documentation?

LarryC

Intel(R) Core(TM) i7 CPU         960  @ 3.20GHz (SSE4)

24159   cycles for 100 * rtrimUltra (Masm32 len)
17598   cycles for 100 * rtrimUltra (MasmBasic Len)
68562   cycles for 100 * rtrim$

11685   cycles for 100 * rtrimUltra (Masm32 len)
4215    cycles for 100 * rtrimUltra (MasmBasic Len)
68624   cycles for 100 * rtrim$

11795   cycles for 100 * rtrimUltra (Masm32 len)
4337    cycles for 100 * rtrimUltra (MasmBasic Len)
68742   cycles for 100 * rtrim$

36      bytes for rtrimUltra (Masm32 len)
8       bytes for rtrimUltra (MasmBasic Len)
13      bytes for rtrim$

hutch--

This old quad has made a clown out of me many times with timings, after writing a second algo that has a shorter instruction path and writing a method of benchmarking the difference, the two algos stubbornly refuse to yield any difference in timing. The technique allocates very large amounts of memory as an array of strings then tests each algo. Timings on 1.5 gig of memory yield 250 ms (32 million strings of 48 bytes each), I have dropped the count to 12 million so more people could run it without allocating so much memory but it yields timings as follows,


Allocating 589824 megabytes for testing
If it crashes due to lack of memory, reduce the allocation
size in the 'lpcount' equate above
93 milliseconds rtrim1
94 milliseconds rtrim2
93 milliseconds rtrim1
94 milliseconds rtrim2
94 milliseconds rtrim1
94 milliseconds rtrim2
94 milliseconds rtrim1
94 milliseconds rtrim2
Press any key to continue ...


With just on 590 mem of memory with 12 million strings running under 100 ms, its a case of who cares.

Here is the test bed.


IF 0  ; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤
                      Build this template with "CONSOLE ASSEMBLE AND LINK"
ENDIF ; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

    include \masm32\include\masm32rt.inc

    rtrim1 PROTO :DWORD         ; first test algo
    rtrim2 PROTO :DWORD         ; second test algo

    .data
      item1 db "this is a test of rtrim algos                  ",0  ; 48 bytes total
      item2 db "       this is a test of rtrim algos           ",0  ; 48 bytes total
      item3 db "              this is a test of rtrim algos    ",0  ; 48 bytes total
      item4 db "                                               ",0  ; 48 bytes total

    .code

start:
   
; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

    call main
    inkey
    exit

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

    lpcount equ <1024*1024*12>

main proc

    LOCAL txt   :DWORD
    LOCAL hArr  :DWORD
    LOCAL pArr  :DWORD
    LOCAL aInd  :DWORD
    LOCAL lcnt  :DWORD
    LOCAL icnt  :DWORD

    push esi
    push edi

    print "Allocating "
    print ustr$(lpcount*48 / 1024)," megabytes for testing",13,10
    print "If it crashes due to lack of memory, reduce the allocation",13,10
    print "size in the 'lpcount' equate above",13,10

    invoke SetPriorityClass,rv(GetCurrentProcess),HIGH_PRIORITY_CLASS

  mov icnt, 4
  tlp:

  ; ******************************************
  ; write an array of strings
  ; ------------------------------------------
    mov eax, lpcount
    shr eax, 2
    mov lcnt, eax

    mov pArr, rv(create_array,lpcount,48)       ; pointer array handle
    mov hArr, ecx                               ; main array memory handle
    mov edi, pArr

  larr1:
    cst [edi],    OFFSET item1
    cst [edi+4],  OFFSET item2
    cst [edi+8],  OFFSET item3
    cst [edi+12], OFFSET item4
    add edi, 4
    sub lcnt, 1
    jnz larr1
  ; ------------------------------------------

    invoke GetTickCount
    push eax

    mov eax, lpcount
    shr eax, 2
    mov lcnt, eax

  tarr1:
    mov txt, rv(rtrim1,[edi])
    mov txt, rv(rtrim1,[edi+4])
    mov txt, rv(rtrim1,[edi+8])
    mov txt, rv(rtrim1,[edi+12])
    add edi, 4
    sub lcnt, 1
    jnz tarr1

    free pArr
    free hArr

    invoke GetTickCount
    pop ecx
    sub eax, ecx

    print ustr$(eax)," milliseconds rtrim1",13,10

  ; ******************************************
  ; write an array of strings
  ; ------------------------------------------
    mov eax, lpcount
    shr eax, 2
    mov lcnt, eax

    mov pArr, rv(create_array,lpcount,48)       ; pointer array handle
    mov hArr, ecx                               ; main array memory handle
    mov edi, pArr

  larr2:
    cst [edi],    OFFSET item1
    cst [edi+4],  OFFSET item2
    cst [edi+8],  OFFSET item3
    cst [edi+12], OFFSET item4
    add edi, 4
    sub lcnt, 1
    jnz larr2
  ; ------------------------------------------

    invoke GetTickCount
    push eax

    mov eax, lpcount
    shr eax, 2
    mov lcnt, eax

  tarr2:
    mov txt, rv(rtrim2,[edi])
    mov txt, rv(rtrim2,[edi+4])
    mov txt, rv(rtrim2,[edi+8])
    mov txt, rv(rtrim2,[edi+12])
    add edi, 4
    sub lcnt, 1
    jnz tarr2

    free pArr
    free hArr

    invoke GetTickCount
    pop ecx
    sub eax, ecx

    print ustr$(eax)," milliseconds rtrim2",13,10

  ; ******************************************

    sub icnt, 1
    jnz tlp

    invoke SetPriorityClass,rv(GetCurrentProcess),NORMAL_PRIORITY_CLASS

    pop edi
    pop esi

    ret

main endp

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

OPTION PROLOGUE:NONE
OPTION EPILOGUE:NONE

rtrim1 proc ptxt:DWORD

  ; ------------------------------------------------
  ; one pass left to right to determine last valid
  ; character then terminate it after that character
  ; ------------------------------------------------

    mov edx, [esp+4]                ; load address into EDX
    mov ecx, edx                    ; store that address in ECX
    sub edx, 1

  lpst:
    add edx, 1
    movzx eax, BYTE PTR [edx]       ; zero extend byte into EAX
    test eax, eax                   ; test for zero terminator
    jz lpout                        ; exit loop on zero
    cmp eax, 32                     ; test if space character or lower
    jbe lpst                        ; jump back if below or equal
    mov ecx, edx                    ; store updated last character location in ECX
    jmp lpst                        ; jump back to loop start

  lpout:
    cmp ecx, [esp+4]                ; if ECX has not been modified (empty string)
    je nxt                          ; jump to NXT label
    add ecx, 1                      ; add 1 to ECX to write terminator past last character
  nxt:
    mov BYTE PTR [ecx], 0           ; write the terminator

    mov eax, [esp+4]                ; put original stack address in EAX as return address
    ret 4                           ; BYE !

rtrim1 endp

OPTION PROLOGUE:PrologueDef
OPTION EPILOGUE:EpilogueDef

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

OPTION PROLOGUE:NONE
OPTION EPILOGUE:NONE

rtrim2 proc ptxt:DWORD

  ; ------------------------------------------------
  ; one pass left to right to determine last valid
  ; character then terminate it after that character
  ; ------------------------------------------------

    mov edx, [esp+4]                ; load address into EDX
    mov ecx, edx                    ; store that address in ECX
    jmp ji

  pre:
    mov ecx, edx                    ; store updated last character location in ECX
  lp1:
    add edx, 1
  ji:
    movzx eax, BYTE PTR [edx]       ; zero extend byte into EAX
    cmp eax, 32                     ; test if space character or lower
    ja pre
    test eax, eax                   ; test for zero terminator
    jnz lp1                         ; fall through on zero

  lpout:
    cmp ecx, [esp+4]                ; if ECX has not been modified (empty string)
    je nxt                          ; jump to NXT label
    add ecx, 1                      ; add 1 to ECX to write terminator past last character
  nxt:
    mov BYTE PTR [ecx], 0           ; write the terminator

    mov eax, [esp+4]                ; put original stack address in EAX as return address
    ret 4                           ; BYE !

rtrim2 endp

OPTION PROLOGUE:PrologueDef
OPTION EPILOGUE:EpilogueDef

; ¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤¤

end start