15
\$\begingroup\$

I've just started learning assembly yesterday, and the first useful thing I've written is a clearmem function.

I'm looking for general feedback regarding my coding of this function, whether there any flaws with it other than the obvious one of a user passing a value <= 0 to the size argument, or an invalid pointer to the ptr argument (should I really even check for that in practical use)?

I also observed an odd behavior that I still don't quite understand, so if you can explain that, then that would be much appreciated as well.

Intel x64 Assembly - on Linux

clearmem:                         ; void clearmem( void* ptr, long size )
    mov rcx, rsi                  ; copy rsi/size to rcx (aka the counter register)
    next:                         ; for( long i = size; i > 0; i-- )
        mov byte [rdi+rcx-1], 0   ;    *(reinterpret_cast<char*>(ptr+i-1)) = 0;
    loop next                     ; dec rcx; cmp rcx, 0; jg next
    ret                           ; return

Intel x64 Assembly - on Windows

clearmem proc
    xchg rcx, rdx                 ; swap arg1 and arg2... windows uses rcx for arg1, and rdx for arg2
    next:
        mov byte ptr[rdx+rcx-1], 0
    loop next
    ret
clearmem endp

Example usage in C++

#include <iostream>
// ...
extern "C" void clearmem( void* ptr, long size );
// ...
int main( int argc, char* argv[] )
{

    int z[] = {1,2,3,4,5,6,7,8,9,10};
    int zlen = sizeof(z) / sizeof(int);

    std::cout << "z[] before clearmem() = ";
    print_array<int>(z, zlen);

    clearmem(&z, sizeof(z));
    std::cout << "z[] after clearmem() = ";
    print_array<int>(z, zlen);

    return 0;
}

Output of C++ program

z[] before clearmem() = 1,2,3,4,5,6,7,8,9,10
z[] after clearmem() = 0,0,0,0,0,0,0,0,0,0

Odd Behavior

If I change all of the registers and variables to their 4 byte equivalent (e.g. rcx -> ecx, long -> int) then it still works fine in Windows, but on Linux it will segfault. I used GDB to set a break point on mov byte[rdi+rcx-1], 0 and used info registers to see the values in the registers and they were extrmely high. For instance rdi was 0x00007FFFFFFFE6E0 at the time, and considering I only have 16 GB of RAM, I would have expected a value less than 0x0000000400000000.

What's going on here? Why does this work on Windows, but not on Linux? Note: Obviously, I know I shouldn't do this, but I like breaking things...

\$\endgroup\$
13
\$\begingroup\$

I can't say I particularly like this code as it is right now. It seems to me that there are two reasonable approaches: if you think most of what you zero will be in main memory, then you probably just want the most compact code possible for the job. If you think it'll be used to zero data that might be in the cache a noticeable amount of the time, then you want to optimize for speed.

For the former case, I'd probably do something like:

mov rcx, rsi
xor rax, rax
rep stosd

Recent Intel (and, I believe, AMD) CPUs seem to have a special execution path that lets this run just about as fast as anything--if any more than a minuscule percentage of your data may be in main memory, it becomes difficult to measure the difference between this and the fastest code possible.

If you really do want the fastest code possible, then you probably want to avoid complex instructions like loop, which is known to be sub-optimal on recent processors (anything since the Pentium Pro, really). For this, you want to use simple instructions for the loop:

dec rcx
test rcx, rcx
jnz loop_top

Although the cmp rcx, 0 that you suggest in the comment will work, embedding the immediate value in the instruction makes it somewhat larger and (depending on CPU) somewhat slower to fetch. The test accomplishes the same goal (sets/clears Z-flag appropriately) without an immediate value. In this case, you probably want to zero an xmm register, then start with a (possible) maskmovdqu to fill in a partial word, in case the data is mis-aligned. Then do the main body of the loop using movdqa. Then have another (again, optional) maskmovdqu at the end to copy an partial-word tail that might be needed.

If you're writing for an Intel processor (but not AMD) it's also worth considering a move to using AVX instructions instead. The current AVX instructions operate on 256-bit operands, and Intel has announced an AVX-512 instruction set that will (obviously enough) operate on 512-bit operands. For operands in main memory, you'll be limited by memory bandwidth either way, but if the data is in cache, this should give a substantial speedup. As far as AMD goes: they do have processors that can execute the current AVX instruction set, but it seems to be poorly enough tuned that performance gains from using it are minimal (at best). Then again, I haven't tried to test using them specifically for zeroing data--for such a simple task, it's possible it'll do better (but from what I recall, loads and stores seem like the bottleneck).

\$\endgroup\$
  • \$\begingroup\$ The test is redundant. Use dec rcx / jnz loop_top, because dec already sets ZF according to the result. It dec/jnz can even macro-fuse on Intel SnB-family microarchitectures. BTW yes, loop is slow on most CPUs, except AMD Bulldozer-family. And yes, rep stosq is highly recommend, as long as the buffer is aligned. Otherwise, do one unaligned store then rep stos. (See also stackoverflow.com/tags/x86/info for more perf links) \$\endgroup\$ – Peter Cordes Aug 26 '16 at 3:45
  • \$\begingroup\$ Note that the special path seems to be only for stosb. The instruction stosd is still slow as far as I am concerned. \$\endgroup\$ – FUZxxl Apr 1 '18 at 19:11
13
\$\begingroup\$
  • The comments in the clearmem procedure for the Linux block look a bit confusing. You could just have a summary of the procedure commented above, and have the individual comments for each line specify the meaning of the assembly instructions.

    Specifically, the lines that describe the C++ code don't quite reflect on the assembly code. clearmem itself doesn't take two arguments. The for loop comment statement doesn't seem needed, and it may be clearer to specify when the loop should exit. You also don't need the obvious comment for ret.

  • Since it appears the extern "C" corresponds to clearmem, I assume everything else can fully utilize C++. If so, I'd use std::vector in place of the C-style arrays. However, since clearmem performs pointer arithmetic, you should use std::vector::data() to pass in the underlying dynamic array.

    You can then call size() for the vector object in main() instead of using sizeof. This function returns an std::size_type, specifically std::vector<int>::size_type.

  • I'm not sure how print_array() is implemented, but I assume it just uses a for loop to print the array elements. You might also not need the template argument, and with just a vector, you'll have just one argument. In the function's loop, you should then use const_iterators for displaying the elements, although using indices is okay (but iterators are still preferred).

  • clearmem should take size_t instead of long; the former is the same type returned by the sizeof operator.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.