Log in

View Full Version : Runtime assembler generation!


Pages : [1] 2

sh0dan
26th March 2003, 19:30
I just finished a test of a program I recently found called SoftWire (http://sourceforge.net/projects/softwire/ ).

It is a runtime assembler compiler - meaning it can generate code, either from text files or by using 'run-time intrinsics'. This means that it can generate code, at runtime, by being called from another program!

This is very interesting in the world of assembler. This opens up the possibility for creating highly optimized assembler for each filter. It is possible to optimize the code very precise for the task it has perform. It will also make it possible to adjust the code to each CPU without having to maintain several different routines.

The test I made was to rewrite the rather simple Limiter function - unrolling loops, prefetching based on the detected CPU.
What basicly happends is that when the first frame is requested, assembler code is being generated to run the filter on this specific image size. This code is saved and used for all the following frames.

Basicly the code looks like this:

BYTE* Limiter::create_emulator(void* entry, int row_size, int height, int modulo, IScriptEnvironment* env) {

int mod32_w = row_size/32;
int remain_4 = (row_size-(mod32_w*32))/4;

int prefetchevery = 1;
if ((env->GetCPUFlags() & CPUF_3DNOW_EXT)||((env->GetCPUFlags() & CPUF_SSE2))) {
// We have either an Athlon or a P4
prefetchevery = 2;
}

bool use_movntq = true;
if ((env->GetCPUFlags() & CPUF_3DNOW_EXT)) {
// Experimentally disable movntq on Athlon
use_movntq = false;
}

Assembler x86; // This is the class that assembles the code.
BYTE* ret; // This is a pointer to the generated code (not the entry point though).

if (env->GetCPUFlags() & CPUF_INTEGER_SSE) {
x86.push(eax);
x86.push(ebx);
x86.push(ecx);
x86.push(edx);
x86.push(esi);
x86.push(edi);

x86.mov(eax, height);
x86.mov(ebx, dword_ptr [&c_plane]);
x86.mov(ecx, modulo);
x86.movd(mm7,dword_ptr [&emu_cmax]);
x86.movd(mm6, dword_ptr [&emu_cmin]);
x86.pshufw(mm7,mm7,0);
x86.pshufw(mm6,mm6,0);

x86.align(16);
x86.label("yloop");

for (int i=0;i<mod32_w;i++) {
// This loop processes 32 bytes at the time.
// All remaining pixels are handled by the next loop.

if (!(i%prefetchevery)) {
//Prefetch only once per cache line
x86.prefetchnta(dword_ptr [ebx+256]);
}

x86.movq(mm0,ebx);
x86.movq(mm1,ebx+8);
x86.movq(mm2,ebx+16);
x86.movq(mm3,ebx+24);
x86.pminub(mm0,mm7);
x86.pminub(mm1,mm7);
x86.pminub(mm2,mm7);
x86.pminub(mm3,mm7);
x86.pmaxub(mm0,mm6);
x86.pmaxub(mm1,mm6);
x86.pmaxub(mm2,mm6);
x86.pmaxub(mm3,mm6);
if (use_movntq) {
x86.movq(ebx,mm0);
x86.movq(ebx+8,mm1);
x86.movq(ebx+16,mm2);
x86.movq(ebx+24,mm3);
} else {
x86.movntq(ebx,mm0);
x86.movntq(ebx+8,mm1);
x86.movntq(ebx+16,mm2);
x86.movntq(ebx+24,mm3);
}
x86.add(ebx,32);
}

for (i=0;i<remain_4;i++) {
// Here we process any pixels not being within mod32.
x86.movd(mm0,ebx);
x86.pminub(mm0,mm7);
x86.pmaxub(mm0,mm6);
x86.add(ebx,4);
}

x86.add(ebx,ecx);
x86.dec(eax);
x86.jnz("yloop");

x86.emms();
x86.pop(edi);
x86.pop(esi);
x86.pop(edx);
x86.pop(ecx);
x86.pop(ebx);
x86.pop(eax);

x86.ret();

int* t= (int*)entry;
t[0] = (int)x86.callable(); // This is a pointer to where the assembler
// is placed in memory

if(!entry) {
_RPT0(0,x86.getErrors());
env->ThrowError("Limiter: ISSE code could not be compiled.");
}
ret = (BYTE*)x86.acquire();
}
return ret;
}


This is how the assembler gets generated. Executing the code is simply a matter of calling the pointer returned by the function.
As some might see - we are able to unroll the x-loop (using the for loop). We are able to put in special code, adjusting to the image size. We adjust prefetch distance based on the current CPU.

The only downside is that all data that changes between each run (plane pointers for instance) has to be stored as member variables, as we cannot pass them as function parameters (I think).

I hope very much to be able to use this in further optimizing AviSynth. I put up the updated files for this test in assembler_test.zip (http://cultact-server.novi.dk/kpo/avisynth/assembler_test.zip). Unzip the files in the 2.5 CVS source to test!

What's the judgement of my fellow assemblers here? Usefull?

Bidoche
26th March 2003, 21:23
Sounds great.
Seems better than choosing between all the CPU different codepaths.

A few remarks :
- Are you unrolling the x loop is worth ? I mean it's a big loop and it will be repeated generally easily ten times.
(Maybe we should add a flag somewhere whether we want speed or space optimization)
- I guess you will use an asm block to call entry, it should be possible to stack parameters in this block for it to use, or no ?
- Your code has two return values, ret and entry. In a C++ vision of things, you should define a class to hold the both of them.

class DynamicAssembledCode {

auto_ptr<BYTE> ret; //automatic deletion with ownership transfer (I assume new has been used to memory allocation
BYTE * entry; //I am not sure of type here :p

public
DynamicAssembledCode(const Assembler& x86, const char * err_msg = "") : entry(x86.callable())
{
if(! entry)
{
_RPT0(0,x86.getErrors());
env->ThrowError(err_msg);
}
ret = x86.acquire();
}

void Call() const { //proper asm block who do the proper thing }
};

and you end your function by return DynamicAssembledCode(x86, "Limiter: ISSE code could not be compiled.");

and you make your function return one of those

Using a template parameter list it should be even possible to define versions where Call takes parameters (assuming of course it is possible)

gabest
27th March 2003, 11:54
OMG, assembly becomes an interpreted language. Incredible :)

sh0dan
27th March 2003, 12:00
Regarding unrolling:
There are also some penalties - especially with massive unrolling. The code cache is 64kb on Athlon - if we make code bigger than this we will get severe penalties. This is however still far from the case on the example, but if we also unrolled the y-loop I'm pretty sure it would.

Otherwise there are some good things about conditional unrolling.
- We do not have to keep a loop counter (= one more free register).
- We don't get branch mispredictions, as there are no branching.
- Loops can be made much bigger (processing more pixels in parallel), because we don't have to care about mod-X processing.

Other pros:
- We can put pointer references and per instance constants directly into the assembler code (= less cache/memory overhead).

The current code is only a proof-of-concept. I don't think it would be that much faster in real life, since it is mostly limited by memory speed. Running multiple filter instances after eachother it will almost surely be slower (due to the code cache size) - but luckily this is not a realistic test setting.

I'm currently rewritting the YV12 Horizontal resizer to generated code. This filter should benefit a lot from this transition, if my assumptions are correct - so until this is done, the actual performance benefit remains a mystery. :)

That class seems very nice indeed! I'll be sure to include it. There should also be a destructor destoying the byte-code.

Regarding parameter passing - it doesn't really matter that much, because accessing them is quite difficult - so using pointers to private member variables are actually easier to write and debug.

Bidoche
27th March 2003, 15:12
Regarding parameter passing - it doesn't really matter that much, because accessing them is quite difficult - so using pointers to private member variables are actually easier to write and debug. Is that so hard to pass parameters to asm ?
I mean if keeping track of where they are in the stack is hard, you can copy them in reserved places in the code, no ?

sh0dan
27th March 2003, 15:21
Right - however I think it is very unflexible to work with. Right now I don't mess with the stack (except I push/pop the current registers in the code above).
MASM does this work for you when writing inline assembler - much easier than writing naked assembler functions where you have to keep track of esp offsets. Just a matter of being able to change the code - besides I think the code is much more readable, when refering variable names and not esp offsets.

sh0dan
27th March 2003, 15:36
A quick question, when doing:

DynamicAssembledCode(const Assembler& x86, IScriptEnvironment* env, const char * err_msg = "") :
entry(x86.callable())
{
if(!entry)
{
_RPT0(0,x86.getErrors());
env->ThrowError(err_msg);
}
ret = x86.acquire();
}


I get this error message:
D:\dev\avisynth2\avisynth\resample.h(56) : error C2320: expected ':' to follow access specifier 'resolved identifier'.

The auto_ptr doesn't work either - just changed it to BYTE* - so I guess I'll have to deallocate it manually.

Edit: ok - just missed a ":" after public. One down ;)

Bidoche
27th March 2003, 18:50
The auto_ptr is crucial, it won't work without it (unless your reinvent its functionality).

You are just lacking the correct #include, add :
#include <memory> //for auto_ptr
using namespace std; //auto_ptr is in the std namespace
auto_ptr handles the destruction of the underlying pointer and the transmission of ownership, with a raw pointer, you will end destroying he code before having a chance to execute it.

Explanation :
//.... in your function, x86 methods calls
return DynamicAssembledCode(x86); //create a TEMPORARY object
//outside the function call
DynamicAssembledCode code = YourFunction(...); //temporary object copied into code and then DESTROYED

So, without ownership transfer semantic, you destroy the generated code immediately.

NB: change ret = ... by ret.reset( ... ); //my mistake
If don't work too use ret = auto_ptr<BYTE>( ... ); //VC6 STL being not compliant (but with STLPort it should be fine)


Edit: Can avoid auto_ptr if we give up copiable semantic, but it would force to initialize properly.
But then it would be possible to make your own custom subclass, directly building the right code, and embedding the parameters as members
#include <boost/scoped_ptr.hpp>

class DynamicAssembledCode {

boost::scoped_ptr<BYTE> ret; //provide the noncopyable semantic naturally
BYTE * entry;

protected:
void Initialize(const Assembly& x86, IScriptEnvironment * env, const char * msg = "")
{
entry = x86.callable();
if(entry == NULL)
{
_RPT0(0,x86.getErrors());
env->ThrowError(msg );
}
ret.reset( x86.acquire() );
}

public:
DynamicAssembledCode() { }

void Call() const {
//if ret.get() == NULL exception
//asm jump entry
}
};

class MyAssembledCode : public DynamicAssembledCode {
public:
MyAssembledCode(IScriptEnvironment * env)
{
Assembler x86;
//x86. ....
Initialize(x86, env, "failure in MyAssembledCode");
}

int paramUsedInMyCode;
BYTE * anotherOne;
};
If not stacking parameters, that would be the clean way.

It supposes code generation at construction, but if you want it at first call, it can be done too (need polymorphism)

sh0dan
27th March 2003, 19:38
Quick update:

I finished the resampler - I'll be doing some performance tests on it later tonight. For now I use this class:

class DynamicAssembledCode {

BYTE* ret;
void (*entry)();

public:
DynamicAssembledCode() {ret = 0;};
DynamicAssembledCode(Assembler &x86, IScriptEnvironment* env, const char * err_msg = "") {
entry = (void(*)())x86.callable();
if(!entry)
{
_RPT0(0,x86.getErrors());
env->ThrowError(err_msg);
}
ret = (BYTE*)x86.acquire();
}

~DynamicAssembledCode() {
}

void Call() const {
if (ret) entry();
}
};

It works fine, except I still have to deallocate the actual code.

I'll put up the modified resampler as soon as I have tested it a bit more - it still behaves strange in some circumstances.

Bidoche
27th March 2003, 19:52
Casting the entry point to a function pointer, definitely smart :)
(Would have wanted finding it myself :p)

sh0dan
27th March 2003, 21:36
First tests: (Colorbars - only horizontal resizer)

With dynamic compilation:
Pass 1/1, compression time 00:00:33.582 (59.59fps)

With ordinary resizers:
Pass 1/1, compression time 00:00:38.874 (51.47fps).

Athlon XP 2200+

Nice enough speedup considering the algorithm is the same.

avih
27th March 2003, 22:55
@sh0dan

i'm not updating myself with assembly news (i mostly program c/c++/web-related), but your solution for run time optimizations seems pretty simple, yet very effective. that's what i call an elegant solution.

imho, this could make a revolution in the optimization field.

cheers for the concept and implementation.
avih

MfA
28th March 2003, 00:08
Softwire is cool, if only someone added the ability for it to do dynamic inlining of these functions (needs binary translation unfortunately). Then finally compiler specific inline assembly could be done away with completely.

Richard Berg
28th March 2003, 01:12
Very cool. This is the sort of solution I was dreaming of with Fastlib before I got caught up in the technicalities of individual CPUs. I'm sure the experienced assemblers here will find plenty of room for improvements.

Question: the way you're compiling it in now, will these tools be available to plugin authors?

sh0dan
28th March 2003, 10:06
@Richard: Yes -it much better to be able to adjust the filter using C-language instead of defines or having multiple code-paths to maintain.

It will not be available through AviSynth, since it is distributed as a library, but it is very easy to add to plugins. Just have a look at the zip-file below to see how it currently links (I'll move the header files to another directory before comitting anything).


The resizer is almost done - only two stupid pixels in the upper left corner of the U-plane are bugging me. :angry:
If you are interested int the code, I added it to the zipfile (http://cultact-server.novi.dk/kpo/avisynth/assembler_test.zip) above.

@Mfa: In principle Softwire code is very easy to port - it is currently gcc compatible, so in fact both optimizations mentioned should be very easy to port. This actually does work like dynamic inlining (unless I'm talking about something else of course).


My main conclusions:

- You can produce very fast and very dynamic code!
- Much easier for specific processor optimizations.
- Debugging is a little harder, but it is still possible to place "int 3" breaks in the code, to break out to the debugger.
- It takes a little longer to actually write the code due to the extended syntax. But if you don't touch "ebp" you can still see variables, etc.

MfA
28th March 2003, 12:19
When I say inlining I mean functions being used in the code without function calls ... softwire works with function pointers. Using this to replace abs() for instance probably isnt a good idea.

sh0dan
28th March 2003, 13:27
No - it cannot modify the compiled code. The only way for this to happend is in JIT compiled environments like Java, where "unoptimized" code is delivered to a native compiler, that has to do the actual compilation.

MfA
28th March 2003, 13:49
It is possible, it is just not easy :)

Look at Dynamo (http://www.hpl.hp.com/techreports/1999/HPL-1999-78.html), or even etch (http://www-kimera.cs.washington.edu/) (although that isnt dynamic/in-memory, but it did perform profile guided out/in-lining on existing binaries). Restricting it to always doing inlining of a assembly function if asked to, without all the profiling and other forms of optimization, would be vastly less complex than Dynamo/etch.

Belgabor
28th March 2003, 14:24
Um, call me naive, but what would be the advantage of inlining over a function call? Dunno much about assembler, but wouldnt the speed gain be neglectible?

MfA
28th March 2003, 14:52
For functions which use less than a couple of dozen of cycles (sad8 when running from cache for instance, or any compiler intrinsics) function call overhead is significant.

"All" you would have to do is to declare mixed C(++)/softwire functions so you can optimize them at runtime. At runtime after parsing the softwire functions destined for inlining, but before assembly, disassemble the mixed functions (caching this disassembled code in external files or the executable itself would be inelegant in the first case, and a little more difficult in the second ... but faster). Insert the softwire assembly functions at their call sites in the disassembled code, run a peephole optimization to remove the function call overhead (you can probably borrow this from LCC) and assemble the result. Let the original function redirect to the new one and presto ... easy ;)

sh0dan
31st March 2003, 11:13
The new SoftWire functions have been put in CVS. I did a considerable amount of testing on the resizers, and it appears to be stable (and faster). Only the YV12 resizer has been updated.

If anyone have problems compiling, just let me know.


@MfA: There are (almost) no cases where this applies to the current AviSynth. Most functions process at least one line of data, so the function call overhead is mostly minimal. Writing a compiler is definately overkill.

trbarry
31st March 2003, 15:37
sh0dan -

What kind of performance improvement did you get with your resize experiment? Is it enough to justify the complication?

- Tom

Bidoche
31st March 2003, 15:42
cool :)
I noticed you sticked with the first class helper design I made.
The second should be better and cleaner, especially when the dynamic code use arguments (which happens often).


On a different topic, you told me that vfw inputs yv12 (and i420) frames with a mod 4 pitch.
I guess it's mod 4 for luma and mod 2 for chroma, or not ?

MfA
31st March 2003, 16:02
I only meant that if it was added then it would be usefull for every conceivable bit of x86 assembly programming, so that "almost" is enough reason for me to want to see it added (it is more relevant to xvid for instance, which entirely foregoes inline assembly). Maybe Nopcodes/Hyperopt (http://www.codecs.org/) will be able to do it someday.

sh0dan
31st March 2003, 16:19
@trbarry: There wasn't much more to be squeezed out of the resizer, but I'll estimate it is ~10% faster - probably even more one P4 (due to the larger branch-mispredict penalties.
Actually the code is often cleaner when writing it this way - it's takes longer time to actually write this syntax, but compared to having to mailtain multiple codepaths, it is much nicer.

@Bidoche: I'd rather stick to something I know how it works - and relying on external libraries. Having to do a manual deallocation is worth the price of not having to learn something new. (yes I am being ironic!) ;)
However parameters are not really needed - when it is possible to put pointers to data directly into the assembler this is often both faster, and definately easier to use than esp offsets.

On the VFW issue you can still recieve non-mod4 video from an AVI file - it only pads (luma) pitch up to mod4. Non-mod2 is however not an option.

Bidoche
31st March 2003, 16:41
and relying on external libraries. Should not be "not relying" ?
Having to do a manual deallocation is worth the price of not having to learn something new. (yes I am being ironic!)
Manual deallocation are evil (risky)
However parameters are not really needed - when it is possible to put pointers to data directly into the assembler this is often both faster, and definately easier to use than esp offsets.
You misunderstood me, I am no longer arguing in favor of passing params through the stack, the code is definitely easier to write and understand the other way.
My point is that they should be part of the DynamicAsssembledCode class.
class DynamicFilteredResizeH : private DynamicAssembledCode {

BYTE * dstp;
const BYTE * scrp;
//....

public void Call(BYTE * _dstp, const BYTE * _scrp /* ... */)
{
dstp = _dstp;
srcp = _srcp;
//....
Call();
}
};
Besides this design would allow to reuse the classes made, since they would no longer tied to the filter who use them.
You are not planning to write a Create_Dynamic_BitBlit for each filter who use it, I hope ?

With this design, we can make a (or more) DynamicAssembledBitBlit subclass with all the possible specialisations and use it everywhere

Bidoche
31st March 2003, 16:49
On the VFW issue you can still recieve non-mod4 video from an AVI file - it only pads (luma) pitch up to mod4. Non-mod2 is however not an option.I was talking about luma and chroma pitchs...

sh0dan
31st March 2003, 17:52
When recieving (and sending) from VFW, chroma pitch is always half of luma pitch. On the other hand, I don't know any VFW codecs actually delivering YV12 data in so special resolutions, so I don't know how it works in real life.

Bidoche
31st March 2003, 22:21
Ok, I finished to reaccommodate videoframe and videoframebuffer hierarchy so vfw can fed them correctly.

c0d1f1ed
2nd April 2003, 12:47
Hi,

To make it more elegant and easy to write run-time intrinsics, you could derive a class from SoftWire::Assembler. Here's some pseudo code to illustrate this:

class Resizer : public Assembler
{
void (*entry)(Frame *in, Frame *out);

void Resizer(bool parameter1)
{
mov(esi, in);
mov(edi, out);

if(parameter1)
{
// ...
}

// ...

entry = callable();
}

void process(Frame *in, Frame *out)
{
entry(in, out);
}
}

This way, the constructor immediately generates the code, and the deallocation is managed by the Assembler's destructor. Also note that you don't have to write "x86." no more in front of every intrinsic. So it's a lot less typing.
Softwire is cool, if only someone added the ability for it to do dynamic inlining of these functions (needs binary translation unfortunately). Then finally compiler specific inline assembly could be done away with completely.
This is easy but... useless. We can't know how long the code will be in advance, so we need to allocate the maximum length we can expect, and use a jump to skip the rest. So it's not very memory efficient, and a jump takes about as long as a return. But even if this problem could be solved, it's still too much trouble for nothing. Instead of only doing the inner part of a loop, write the whole loop in assembly. This way the calling overhead is neglectible compared to the execution time of the whole loop.

Bidoche
2nd April 2003, 13:03
Yes, but this way you keep all the data inherited from Assembler after compilation, when they are no longer needed.
I don't know the size of the Assembler class, but it may use a significative amount of memory.
That's why I made classes who take it as a parameter and let it be destroyed.

But I agree with you, it's more elegant to have everything done in the constructor, that's the case with my second design : the subclass constructor creates, compiles with and eventually destroys the Assembler.

sh0dan
2nd April 2003, 15:51
Just for information:

implemented resizer (http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/avisynth2/avisynth/resample.cpp?rev=1.22&only_with_tag=MAIN&sortby=date&content-type=text/vnd.viewcvs-markup)
Softwire helper class (http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/avisynth2/avisynth/softwire_helpers.cpp?rev=1.1&only_with_tag=MAIN&sortby=date&content-type=text/vnd.viewcvs-markup)
header for the above (http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/avisynth2/avisynth/softwire_helpers.h?rev=1.1&only_with_tag=MAIN&sortby=date&content-type=text/vnd.viewcvs-markup)

Personally I have the tools I require now - doing a manual free doesn't concern me much, but as always you are free to change the stuff, if you can make it even better!

A bigger problem: How do I get my debugging registers back?
When monitoring registers in ordinary inline assembler mm0, eax, etc. refer to the CLASSES not the actual registers - VERY annoying (to say at least).

c0d1f1ed
2nd April 2003, 15:52
I understand. Sorting the instruction set uses 5 kB, but this is shared over all Assembler instances. The loader also keeps a copy of the assembled code in an intermediate form which takes about 31 bytes per instruction. So an inner loop of 160 instructions (which is quite long) would use another 5 kB. The generated code would be about 500 bytes, so relatively it's a big waste. But overall I still think that a few kB is nothing to worry about because you normally don't use more than a few Assembler instances at the same time. In the future I would like to allow re-linking with new parameters so the intermediate code would then have to be preserved.

Anyway, I could change the behaviour of the Assembler::aquire method to release all unnecessary memory. All other calls can then return null. So the etra memory overhead should then be nearly zero. Would that be an interesting feature?

sh0dan
2nd April 2003, 16:00
Actually the resizer code mostly produces between 1000 and 4000 bytes of code (based on image size and the resize filter), because of unrolling, so we are talking quite some instructions.

Bidoche
2nd April 2003, 16:40
@c0d1f1ed

If you can keep the memory overhead after compilation to a few dozens bytes, we may afford inheriting from it.
After all we are manipulating videoframes who counts in hundred Kb, we don't really care about a few bytes here and there.

Besides, a few more questions :
- Is the Assembler class copyable (I'd rather not, but if is I will block it from subclasses)
- Does it free the code by himself ? I mean if I subclass, I'd rather let it handle the memory for the code and make the callable() call only at processing (May need a method to significate code generation is finished then)
- You seem to use malloc (coz shodan used free to liberate), why not new ?


@sh0dan

The problem about changing your code to my liking is that I don't understand it much...

MfA
2nd April 2003, 20:35
c0d1f1ed : I replied, but I guess I shouldn't pollute this thread with something unrelated to avisynth with quite that much text ... so I moved it (https://sourceforge.net/forum/forum.php?thread_id=842266&forum_id=184692) to sourceforge.

c0d1f1ed
2nd April 2003, 22:44
Originally posted by sh0dan
A bigger problem: How do I get my debugging registers back?
When monitoring registers in ordinary inline assembler mm0, eax, etc. refer to the CLASSES not the actual registers - VERY annoying (to say at least).
Could you explain this a little more? I never had much trouble debugging my generated code...

sh0dan
2nd April 2003, 22:46
It's not the generated code - it's ordinary inline assembler. I'll post a screenshot in a few minutes.

sh0dan
2nd April 2003, 22:50
Screenshot (http://cultact-server.novi.dk/kpo/avisynth/debug.png)

c0d1f1ed
2nd April 2003, 22:57
Originally posted by Bidoche
- Is the Assembler class copyable (I'd rather not, but if is I will block it from subclasses)
- Does it free the code by himself ? I mean if I subclass, I'd rather let it handle the memory for the code and make the callable() call only at processing (May need a method to significate code generation is finished then)
- You seem to use malloc (coz shodan used free to liberate), why not new ?
At the moment the Assembler class is not copyable, as I don't see any reason why that would be useful. If it really has a use, then I don't think it's much trouble to implement it...

When you only use callable(), the code is destructed by Assembler, also when you use it as a base class. When you use acquire(), Assembler looses the ownership of the generated code. This function returns the pointer to the start of the code (not necessarily the entry point) and then it is your task to delete it.

I haven't checked it, but I don't think I use malloc anywhere. But it's no problem to delete the code with free() because the memory is just a block of bytes instead of classes which need to be destructed.

c0d1f1ed
2nd April 2003, 23:18
Thanks for that screenshot sh0dan. I now remember having the same problem once...

I'm afraid this is a 'feature' of Visual C++ 6.0 and .NET :rolleyes: A weird thing is that it sometimes doesn't occur after a call...

A possible workaround could be to make the register names upper case, but that's of course an ugly way to avoid the problem. Another possibility is to copy the registers to memory to get their value, but again that's far from elegant.

If you got any other idea's, please let me know!

sh0dan
2nd April 2003, 23:27
Upper case will do just fine for now. The strange thing is that this module doesn't even have Softwire headers referenced, but I guess it's just something to get used to.

Thanks again!

c0d1f1ed
2nd April 2003, 23:37
I just found another workaround, and this time it's a lot more elegant!

Just press Alt+5 ;)

Bidoche
3rd April 2003, 00:39
At the moment the Assembler class is not copyable, as I don't see any reason why that would be useful. Precisely, I want it to be non-copyable.
When you only use callable(), the code is destructed by Assembler, also when you use it as a base class. When you use acquire(), Assembler looses the ownership of the generated code. This function returns the pointer to the start of the code (not necessarily the entry point) and then it is your task to delete it. Subclasses are just fine then, as soon as there is a way to release the unused memory after compile

I haven't checked it, but I don't think I use malloc anywhere. But it's no problem to delete the code with free() because the memory is just a block of bytes instead of classes which need to be destructed. I assumed that because in the helper class I made, shodan completed the destructor code using free. If you used new, it should be delete.
Operator new and malloc are different even for just raw memory.
In some implementations of the C++ standard they may be same (VC6 probably since it worked), but the standard allows new to introduce some smarter management : ie allocate large block through malloc to divide into smaller, in order to limit fragmentation.

c0d1f1ed
3rd April 2003, 11:33
Originally posted by Bidoche
Subclasses are just fine then, as soon as there is a way to release the unused memory after compile
I just checked that this is possible by calling Assembler::~Assembler() in your subclass. It might create some trouble when using intrinsics after this point though...

Thanks for the explanation of free vs. delete. I didn't know they could have different implementations even for raw memory.

c0d1f1ed
4th April 2003, 20:01
I've committed a new version of SoftWire to the CVS. It now returns silently when trying to use any of the Assembler's methods after calling its destructor explicitely.

So now you can safely derive from the Assembler class to have handier access to the intrinsics. After getting the callables and acquiring the generated code you can call Assembler::~Assembler() to minimize memory utilisation.

Bidoche
5th April 2003, 11:53
I don't like having to call the destructor explicitly, sounds more like a hack than anything...
Can't you simply add a method to release the memory used for compilation ?

Besides the destructor will destroy the compiled code unless the subclass acquire it, which will be redundant.


I've committed a new version of SoftWire to the CVS. It now returns silently when trying to use any of the Assembler's methods after calling its destructor explicitely. better throw std::logic_error

c0d1f1ed
5th April 2003, 16:35
I don't like having to call the destructor explicitly, sounds more like a hack than anything...

Can't you simply add a method to release the memory used for compilation ?
Well I could easily write a function release() but then my destructor would look like:

Assembler::~Assembler()
{
release();
}

which seems a bit silly to me. Or is there a special reason why the destructor shouldn't be called explicitely like an ordinary method?

Maybe letting acquire() free the intermediate data was a better idea?better throw std::logic_error
They simply return zero...

Bidoche
5th April 2003, 17:05
Well I could easily write a function release() but then my destructor would look like: But I don't want it to release everything, just the memory used for code generation, the generated code should be keeped, and destroyed only when destructor is called.

If the Assembler superclass don't handle the generated code ownership, there is no real point in subclassing, unless saving some "x86."...
By calling the destructor you mean to use the superclass just as a variable declared in the constructor, and had to do acrobatics to make it possible, that's silly.
And what happens when the subclass implicitly call the superclass destructor ?

Besides it's not because the possibility to call destructors is open, that you can use it without thinking twice.
They are meant to be used in situation like this:
//t of type T defined before
t.~T();
new T(&t); //restore T to default state (used in containers)

Maybe letting acquire() free the intermediate data was a better idea? Just giving up subclassing, I guess. Having an intermediate state where you have code but can no longer generate seems to only complicate matters for you.

They simply return zero... And the client should test if 0 is not returned to know it's working ? And if he doesn't test, it will have unexpected result at a latter time, making it harder to trace.... Exceptions are meant to avoid that.

c0d1f1ed
5th April 2003, 19:54
If the Assembler superclass don't handle the generated code ownership, there is no real point in subclassing, unless saving some "x86."...
I think I finally understand your point :o For me the most important thing was to get rid of the "x86.". I already had to convert large blocks of assembly to run-time intrinsics and it was getting annoying. But I now see that it's also very advantageous to let the baseclass manage the generated code.
Just giving up subclassing, I guess. Having an intermediate state where you have code but can no longer generate seems to only complicate matters for you.
Some compromises can be made. I'm starting to realize that conditional re-linking isn't useful or even possible when using run-time intrinsics. So maybe I'll just have to step away from that idea and delete the intermediate code as soon as the code is generated.
And the client should test if 0 is not returned to know it's working ? And if he doesn't test, it will have unexpected result at a latter time, making it harder to trace...
I'm using exceptions internally, but I thought it would be best to keep the interface as simple as possible. Testing for zero should be common practice and is a well defined way to indicate an error. It's used by lots of other libraries and even the standard implementation of new[] returns zero on failure. I've also heard a lot that some people don't like exceptions. Maybe they are the ones that only know C++ as a better C though :rolleyes: Another reason is that I don't want to force anyone to use my Error class or even an std::exception. Everyone is free to write a little wrapper around Assembler which throws whatever they like to throw :D

Thanks!