Profile your code. Are you simply calling QueryPerformanceCounter 1,000,000,000 times a second or is DoEvents eating all your CPU time?
Any time you call an OS function you are performing an 'int 0x2e' and context change from user mode to kernel mode and invoking all the usual costs of thread scheduling and context changes. Doing that in a loop as fast as you can is not advised. A 1ms sleep is the same thing, you're just throwing your thread into the back of the scheduler every iteration.
Your best bet is to use some kind of high resolution high priority timer interrupt based callback. Set that callback to occur at maybe 900ms seconds (or even sleep for 900ms), then use a more precise method to to fine tune it down to 1000ms exactly. That should use zero CPU because your thread will not even be executed for 90%+ of your wait period. The goal in eliminating CPU usage should be to remove your thread from the scheduler's ready list entirely, while you are waiting. The closer you get your thread to wake up to 1000 ms without going over, and the less iterations you spend actively padding it out to 1000ms with more precise methods, the lower the CPU use will be.
Ideally you want to tune your program to get the thread to wake up within 5 ms of your target timing value so that you can resolve it to 1000 ms and still have time to call the events in that thread quantum (CPU time slice, lasts about 10 ms on Windows IIRC) without losing the CPU to the next thread and having the 1000 ms pass while another thread is running.
Or just use a OS designed exactly for this kind of real time priority like VxWorks if the project allows. It's going to be extremely hard if not impossible to get precise and predictable event timing on a multi tasking GUI driven non-real time OS like Windows. All you can do is get acceptably close and live with it.
Another thing you can do is accumulate the error if the 1 second events are not real time sensitive. For example say every iteration is 1005 ms, every 200 seconds you will have accumulated 1 second of error and you would call your timed event twice. Same goes for subtracting time when the timer is early. It wouldn't be suitable for something real time if those events have to be exactly spaced 1 second apart, but it would guarantee x number of events in x seconds even after numerous days have passed.
Also you are using VB so there may be VM overhead adding to timer errors (the time the VM gets the result from the OS to the time the code in your program evaluating that timer gets interpreted by the VM). A language like Java or VB could literally have thousands of machine cycles pass from one line of program source to the next as the VM cycles through it's main loop while parsing each statement. A language like C on the other hand each set of direct real CPU instructions run on the order of nanoseconds and the functions evaluating the timer conditions are likely to execute much closer to the actual time elapsed by the timer.
As you can see, there are lots of reasons you can expect to never get perfect 1 second timing. What if you nail it at 1000 ms exactly and call your 1 s event handler, only to have that handler suspended by the scheduler at 1005 ms due to it's thread's time slice expiring, then have it resume at 1290 ms? Windows isn't the ideal environment for perfect timing.
Anyhow, if what you are doing works for your needs but is just using too much CPU time, try to sleep for 990 ms then burning 10 ms with a polling loop like you posted. Burn as much time as you can not running at all.
You may want to put your forms event handler in it's own thread as well. There is no need to call it that much, and I assume you only put that in there for app responsiveness inside your timing loop so that the app doesn't freeze in 1 second intervals. Have your DoEvents thread use a non blocking message queue polling function that goes to sleep and wakes when a new message arrives so that you are only calling DoEvents when there are messages to process.