April, 2012

  • The Old New Thing

    What were the tests that WinG did to evaluate video cards?

    • 46 Comments

    Georg Rottensteiner was curious about the weird things that WinG performed on installation to evaluate video cards. "What did it do actually and what for?"

    I don't actually know, since I was not involved in the WinG project, but I remember chatting with one of the developers who was working on video card benchmarks.

    He says that video card benchmarks are really hard to develop, not just because video cards are complicated, but also because video drivers cheat like a Mississippi riverboat card sharp on a boat full of blind tourists.

    He discovered all sorts of crazy shenanigans. Like a video driver which compares the string you ask it to display with the text "The quick brown fox jumps over the lazy dog." If the string matches exactly, then it returns without drawing anything three quarters of the time. The reason: Benchmarks often use that sample string to evaluate text rendering performance. The driver vendors realized that the fastest code is code that doesn't run, so by ignoring three quarters of the "draw this string" requests, they could improve their text rendering performance numbers fourfold.

    That was the only one of the sneaky tricks I remember from that conversation. (I didn't realize there was going to be a quiz 17 years later or I'd have taken notes.) Another example of benchmark cheating was a driver which checked if the program name was TUNNEL.EXE and if so, enabled a collection of benchmark-specific optimizations.

    Anyway, I suspect that the weird things that the WinG installer did were specifically chosen to be things that no video card driver had figured out a way to cheat, at least at the time he wrote the test. I wouldn't be surprised if fifteen seconds after WinG was released, video driver vendors started studying it to see how they could cheat the WinG benchmark...

  • The Old New Thing

    What is the real maximum length of a DNS name?

    • 18 Comments

    The maximum length of a DNS name is 255 octets. This is spelled out in RFC 1035 section 2.3.4. A customer didn't understand why the DnsValidateName was rejecting the following string:

    (63 letters).(63 letters).(63 letters).(62 letters)

    The documentation says

    Returns ERROR_INVALID_NAME if the DNS name

    • Is longer than 255 octets.
    • Contains a label longer than 63 octets.
    • ... other criteria not relevant here...

    The length of the domain name passed in is 63+1+63+1+63+1+62=254 characters, just under the length limit of 255. Why is it rejecting this name that is under the limit?

    Because the limit isn't the number of characters; it's the number of octets.

    Section 3.3 says that a domain-name is represented as a series of labels, and is terminated by a label of length zero. (The label of length zero represents the root label.) A label consists of a length octet followed by that number of octets representing the name itself. Therefore, the domain name www.microsoft.com is encoded as follows:

    3 'w' 'w' 'w' 9 'm' 'i' 'c' 'r' 'o' 's' 'o' 'f' 't' 3 'c' 'o' 'm' 0

    Technically, www.microsoft.com is shorthand for www.microsoft.com. with a trailing period, and the trailing zero byte encodes that implied period.

    If you sit down and do the math, you'll see that the the readable maximum length of an ASCII DNS name is 253 characters: You don't encode the dots, but you do encode the length bytes, so they cancel out, except for the length byte of the first label and the length byte of the root label, for an additional cost of two bytes. (On the off chance that you explicitly specified the root label, don't count it towards the 253-character limit.)

    If you use UTF-8 encoding, then the maximum length is harder to describe since UTF-8 is a variable-length encoding.

  • The Old New Thing

    Introducing the unrolled-switch anti-pattern

    • 39 Comments

    Over the years, I've seen a bunch of coding anti-patterns. I figured maybe I'll share a few.

    Today, I'll introduce what I'm calling the unrolled-switch anti-pattern, also known as "Specialization is always faster, right?"

    enum Axis
    {
        XAxis,
        YAxis,
        ZAxis,
    };
    
    // code earlier in the function ensure that
    // "axis" is always a valid axis
    int newPosition;
    switch (axis)
    {
    case XAxis:
        newPosition = m_position[XAxis] + amount;
        if (newPosition < m_minPosition[XAxis])
            newPosition = m_minPosition[XAxis];
        if (newPosition > m_maxPosition[XAxis])
            newPosition = m_maxPosition[XAxis];
        m_position[XAxis] = amount;
        break;
    case YAxis:
        newPosition = m_position[YAxis] + amount;
        if (newPosition < m_minPosition[YAxis])
            newPosition = m_minPosition[YAxis];
        if (newPosition > m_maxPosition[YAxis])
            newPosition = m_maxPosition[YAxis];
        m_position[YAxis] = amount;
        break;
    case ZAxis:
        newPosition = m_position[ZAxis] + amount;
        if (newPosition < m_minPosition[ZAxis])
            newPosition = m_minPosition[ZAxis];
        if (newPosition > m_maxPosition[XAxis])
            newPosition = m_maxPosition[XAxis];
        m_position[ZAxis] = amount;
        break;
    }
    
    As we all know, special-case code is faster than general-purpose code. Instead of writing slow general-purpose code:

        newPosition = m_position[axis] + amount;
        if (newPosition < m_minPosition[axis])
            newPosition = m_minPosition[axis];
        if (newPosition > m_maxPosition[axis])
            newPosition = m_maxPosition[axis];
        m_position[axis] = amount;
    

    we unroll it into a switch statement, thereby generating highly-optimized special-purpose code, one for each axis.

    What makes this anti-pattern particularly frustrating is that you cannot tell at a glance whether all the cases really are the same (just with different axes).

    In fact, they aren't.

    If you look closely, you'll see that we check the new Z-position against the X-axis maximum rather than the Z-axis maximum. If you're reading this code, you now start to wonder, "Is this a copy/paste bug, or is there some reason that we really do want to check the Z-position against the X-axis minimum?"

    A variation on the unrolled-switch is the unrolled-if, used if the item you want to unroll cannot be used in a switch statement:

    FruitBasket *BananaBasket;
    FruitBasket *AppleBasket;
    FruitBasket *PearBasket;
    FruitBasket *MangoBasket;
    
    if (basket == BananaBasket) {
      if (!BananaBasket->IsEmpty()) {
        fruit = BananaBasket->TakeFruit();
        if (HaveKnife()) {
          TakeKnife();
          fruit->Peel();
          fruit->Slice();
          fruit->Eat();
          ReplaceKnife();
        } else {
          BananaBasket->AddFruit(fruit);
        }
      }
    } else if (basket == AppleBasket) {
      if (!AppleBasket->IsEmpty()) {
        fruit = AppleBasket->TakeFruit();
        if (HaveKnife()) {
          TakeKnife();
          fruit->Peel();
          fruit->Slice();
          fruit->Eat();
          ReplaceKnife();
        } else {
          AppleBasket->AddFruit(fruit);
        }
      }
    } else if (basket == PearBasket) {
      if (!PearBasket->IsEmpty()) {
        fruit = PearBasket->TakeFruit();
        if (HaveKnife()) {
          TakeKnife();
          fruit->Slice();
          fruit->Eat();
          ReplaceKnife();
        } else {
          PearBasket->AddFruit(fruit);
        }
      }
    } else if (basket == MangoBasket) {
      if (!MangoBasket->IsEmpty()) {
        fruit = MangoBasket->TakeFruit();
        if (HaveKnife()) {
          TakeKnife();
          fruit->Peel();
          fruit->Slice();
          fruit->Eat();
          ReplaceKnife();
        } else {
          BananaBasket->AddFruit(fruit);
        }
      }
    }
    

    When I pointed out in an aside to the customer that this could be simplified (after fixing the copy/paste errors) to

    if (!basket->IsEmpty()) {
      fruit = basket->TakeFruit();
      if (HaveKnife()) {
        TakeKnife();
        fruit->Peel();
        fruit->Slice();
        fruit->Eat();
        ReplaceKnife();
      } else {
        basket->AddFruit(fruit);
      }
    }
    

    the response was, "Hey, that's a neat trick. I didn't realize you could do that."

    I wonder if this person also programs loops like this:

    switch (limit)
    {
    case 0:
      break;
    case 1:
      do_something(array[0]);
      break;
    case 2:
      for (int i = 0; i < 2; i++) do_something(array[i]);
      break;
    case 3:
      for (int i = 0; i < 3; i++) do_something(array[i]);
      break;
    case 4:
      for (int i = 0; i < 4; i++) do_something(array[i]);
      break;
    case 5:
      for (int i = 0; i < 5; i++) do_something(array[i]);
      break;
    case 6:
      for (int i = 0; i < 6; i++) do_something(array[i]);
      break;
    ...
    case 999:
      for (int i = 0; i < 999; i++) do_something(array[i]);
      break;
    default:
      FatalError("Need more cases to handle larger array");
      break;
    }
    
  • The Old New Thing

    Microspeak: scoped to

    • 19 Comments

    The Merriam-Webster dictionary gives as the meaning of scope as a verb to look at for evaluation, as in "to scope out the competition." But that's not how we use it at Microsoft.

    Here are some fake citations:

    The Widgets pop-up shows the available widgets scoped to the current selection.
    The results of the search are scoped to the current folder.
    Workflows can be scoped to containers, content types (scopeable to containers, sites, collections, servers, or enterprises), or combinations of these.

    Okay, that last one wasn't fake. You can tell it's not fake because it is extra confusing.

    To be scoped to something is to be limited to or filtered to that thing, or things which apply to that thing. In other words limited in scope to. Here's an attempt to translate those citations into English:

    The Widgets pop-up shows the available widgets which apply to the current selection.
    The results of the search are filtered to those in the current folder.

    I'm not going to try to translate that last one on there. It has this sort of Escherian feeling to it. "Workflows can be scoped to A, or to B (which can be scoped to A), or combinations of A and B (which means combinations of A, and B, and A's within B's?)"

    Another sense of the verb phase scoped to is altered in scope. Usually, the change is to reduce the scope to meet external constraints:

    The proposal has been scoped to meet our December release.

    To emphasize that the scope has been narrowed, you add the adverb down: "scoped down to."

    Translation: "The original proposal was too broad and could not be accomplished within the required time allotted, so we reduced its scope to the point where the parts that remain can be accomplished in time."

    On the other hand, sometimes scope expands.

    Based on customer feedback, the search results have been scoped to include archived data as well.

    And finally, a citation from an official Microsoft job description:

    Oversees the structuring of consulting engagements to ensure they are properly scoped to meet the customer's requirements profitably.
  • The Old New Thing

    Registration-free COM the old-fashioned way: The car mp3 player

    • 29 Comments

    Windows XP introduced Registration-Free COM, permitting you to place your COM object registrations in a manifest rather than in the registry. Very handy when you want to support xcopy deployment or running directly off a thumb drive. And very much in keeping with the principle of not using a global solution for a local problem. (If you need your COM object to be used from other programs, then global registration is not unreasonable, but if the only client is another part of your program, then you have a local problem that should employ a local solution.)

    Here are some articles on the subject:

    Before manifest-based COM object registration was possible, you had to do things the old-school way. Mind you, old-school registration-free COM is not a very useful skill any more, falling into the same category as knowing how to churn butter or use a typewriter eraser, but since when did that ever stop me from writing about something?

    One old-school way is to call Dll­Get­Class­Object directly. This works only if you control both sides of the equation (the client and the server) because it's now your responsibility to ensure that both sides agree on the threading model. You won't have the actual COM libraries sitting in between watching out for scenarios that require marshalling, for example.

    For a toy project of mine, I used a different approach. My little project was an mp3 player for my laptop. Now, sure, we already have tons of mp3-playing apps out there, but mine was different: Mine was designed to be used on long driving trips. (This was back in the days when long driving trips were part of my life. I don't drive that much any more.)

    Here was the plan: I connected the line-out from the laptop into my car sound system, so that the music came out my car speakers. Meanwhile, all input to the program came from the mouse. Specifically, it came from the mouse buttons and the wheel. The mouse itself never moved. The idea was that I could hook up a mouse to the laptop, put the laptop on the passenger seat, and leave the mouse on the center console. I would then use the mouse buttons and wheel to navigate my music collection. I forget exactly what I ended up doing, but it was something like

    • Left button click = select current item
    • Right button click = go up one level
    • Rotate wheel = scroll through current directory

    Now remember, this program was designed to be used while driving, which means both eyes on the road. (This was long before hands-free driving laws came on the scene.) Therefore, the program provided feedback not by displaying items on the screen but by using speech synthesis to read the names of the files and directories out loud. Finding a song to play, therefore, consisted of turning the wheel and listening as the laptop read out the name of the album, then when I found the one I wanted, I would click the left mouse button, and then I would use the wheel to scroll through the songs, and when I heard the title of the one I wanted, I clicked the left mouse button again, and boom, the song started playing.

    I added some heuristics to the code so if consecutive tracks began with the same words (which happens often with classical music, think Symphony #5 in c minor, first movement followed by Symphony #5 in c minor, second movement) it spoke only the changes.

    While the song was playing, the mouse buttons served as playback controls. I think it went something like this:

    • Left button click = pause / play
    • Right button click = exit and choose another song
    • Rotate wheel = rewind / fast-forward ten seconds
    • Press middle mouse button and rotate wheel = previous/next track

    (Exercise: Why didn't I need a volume control?)

    The easiest programming language for this was a Web page. I created a host program that simply created a Web browser control. The host program told the Web browser control to navigate to my carplay.html file, and boom, I now had an in-car playback system. I could use things like File­System­Object to navigate the file system and the Windows Media Player control to do the playback. Now, this story takes place so many years ago that Internet Explorer didn't support the mouse wheel yet, so the host program also converted wheel messages into fake keyboard activity (wheel motion was converted into the up and down arrows) so that the Web page could respond to them.

    Once nice thing about this whole set-up is that I had the HTML DOM at my disposal. My program spewed diagnostic output to the screen like crazy, but who cares? The end user isn't looking at the screen. Therefore, the entire Web page is free real estate for debugging.

    The only thing missing was the speech synthesizer.

    There was no ActiveX control for speech synthesis, so I wrote one of my own. I let SAPI do the heavy lifting; my ActiveX control was simply some glue that let a Web page pass a string to be spoken. (Or pass a null string to shut up.) I wanted my program to be xcopy-deployable (USB thumb drives didn't exist back then) so I looked for a registration-free technique. The Dll­Get­Class­Object technique wouldn't work since I didn't control how Internet Explorer created COM objects; it always called Co­Create­Instance.

    The technique I used was Co­Register­Class­Object. I created a class factory for my object and explicitly registered it before creating the Web browser control. That way, when the Web page asked for my object, lo and behold, there it was: In memory, not in the registry.

    That was a really long-winded story with a punch line that tells you how to do something you don't need to do any more because there are easier ways of doing it nowadays. I wouldn't be surprised if you wanted a refund.

    The real punch line: I spent far more time writing the program than I ever did using it.

  • The Old New Thing

    Why don't I get a Caps Lock warning balloon any more?

    • 32 Comments

    A customer asked for help diagnosing a problem they were experiencing on Windows XP:

    My customer reports that on their machines, they do not get the warning balloon that appears when Caps Lock is set while you are typing into a password field. I searched for relevant KB articles but couldn't find anything related to that. Can you help?

    Time for the psychic powers.

    My psychic powers tell me that the customer disabled all balloon tips.

    The customer liaison replied

    You are right. Thanks for the help.

    This is a not uncommon situation with some customers. They change a setting, and then later report that they're having some problem caused by that setting. They don't bother going to a freshly-installed machine to see whether the problem occurs there as well, in order to isolate whether the problem is related to their customizations or not. They just assume that their customizations couldn't possibly be the cause of the problem, and they are so convinced of this that they don't even mention "Oh, we customized this setting" when they ask for help.

    By the way, the setting to disable all balloon tips in the entire system is a rogue feature. At the time that balloons were originally being developed (and the rules surrounding them being refined based on research and feedback), one developer was impatient with the progress toward making the balloons less annoying, and he just went in and added a rogue feature to disable them with a sledgehammer. And now that people know about it, the rogue feature has become a support burden.

  • The Old New Thing

    I thought I was so clever, salvaging an old floppy drive from a dead computer, but I didn't think *two* steps ahead...

    • 37 Comments

    When one of the oldest computers at Microsoft still doing useful work finally died, I had the presence of mind to salvage the 5¼″ floppy drive from the machine, so that I could (someday) extract the data off all the old 5¼″ floppy discs I have packed away in boxes meaning to convert someday. (Mind you, the data capacity of a giant box of 5¼″ floppy disks is approximately equal to half of a CD.)

    Oh, and by the way, if you know what a floppy drive is, then this question on superuser.com will make you feel old.

    I thought I was so clever, salvaging an old floppy drive from a dead computer so I could use it to rescue data from obsolescence, but that was only thinking one step ahead. I failed to think two steps ahead: Nobody makes motherboards with 5¼″ floppy drive connectors!

    Bonus coincidental posting date: The Geeks Who Saved Prince of Persia's Source Code From Digital Death.

  • The Old New Thing

    You can use an OVERLAPPED structure with synchronous I/O, too

    • 19 Comments

    Even if you didn't open a file with FILE_FLAG_OVERLAPPED, you can still use the OVERLAPPED structure when you issue reads and writes. Mind you, the I/O will still complete synchronously, but you can take advantage of the other stuff that OVERLAPPED has to offer.

    Specifically, you can take advantage of the Offset and OffsetHigh members to issue the I/O against a file location different from the current file pointer. (This is a file pointer in the sense of Set­File­Pointer and not in the sense of the C runtime FILE*.) If your program does a lot of reads and writes to random locations in a file, using the synchronous OVERLAPPED structure saves you a call to Set­File­Pointer at each I/O.

    Let's illustrate this by writing some code to walk through a file format that contains a lot of offsets to other parts of the file: The ICO file format. First, the old-fashioned way:

    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    
    #include <pshpack1.h>
    struct ICONDIRHEADER {
        WORD idReserved;
        WORD idType;
        WORD idCount;
    };
    
    struct ICONDIRENTRY {
        BYTE bWidth;
        BYTE bHeight;
        BYTE bColorCount;
        BYTE  bReserved;
        WORD  wPlanes;
        WORD  wBitCount;
        DWORD dwBytesInRes;
        DWORD dwImageOffset;
    };
    #include <poppack.h>
    
    BOOL ReadBufferAt(__in HANDLE hFile,
        __out_bcount(cbBuffer) void *pvBuffer,
        DWORD cbBuffer,
        DWORD64 offset)
    {
     LARGE_INTEGER li;
     DWORD cbRead;
     li.QuadPart = offset;
     return SetFilePointerEx(hFile, li, nullptr, FILE_BEGIN) &&
            ReadFile(hFile, pvBuffer, cbBuffer, &cbRead, nullptr) &&
            cbBuffer == cbRead;
    }
    
    int __cdecl wmain(int argc, wchar_t **argv)
    {
     HANDLE hFile = CreateFile(argv[1], GENERIC_READ,
      FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
      nullptr, OPEN_EXISTING, 0, nullptr);
     if (hFile != INVALID_HANDLE_VALUE) {
      ICONDIRHEADER hdr;
      if (ReadBufferAt(hFile, &hdr, sizeof(hdr), 0) &&
          hdr.idReserved == 0 && hdr.idType == 1) {
       for (UINT uiIcon = 0; uiIcon < hdr.idCount; uiIcon++) {
        ICONDIRENTRY entry;
        if (ReadBufferAt(hFile, &entry, sizeof(entry),
                         sizeof(hdr) + uiIcon * sizeof(entry))) {
         void *pvData = LocalAlloc(LMEM_FIXED, entry.dwBytesInRes);
         if (pvData) {
          if (ReadBufferAt(hFile, pvData,
                           entry.dwBytesInRes, entry.dwImageOffset)) {
           // process one image in the icon
          }
          LocalFree(pvData);
         }
        }
       }
      }
      CloseHandle(hFile);
     }
     return 0;
    }
    

    Run this program with the name of an icon file on the command line, and nothing interesting happens because the program doesn't generate any output. But if you step through it, you can see that we start by reading the ICON­DIR­HEADER to verify that it's an icon and determine the number of images. We then loop through the images: For each one, we read the ICON­DIR­ENTRY (specifying the explicit file offset), then read the image data (again, specifying the explicit file offset).

    We use the Read­Buffer­At function to read data from the file. For each read, we first call Set­File­Pointer to position the file pointer at the byte we want to read, then call Read­File to read it.

    Let's change this program to take advantage of our newfound knowledge:

    BOOL ReadBufferAt(__in HANDLE hFile,
        __out_bcount(cbBuffer) void *pvBuffer,
        DWORD cbBuffer,
        DWORD64 offset)
    {
     OVERLAPPED o = { 0 };
     o.Offset = static_cast<DWORD>(offset);
     o.OffsetHigh = static_cast<DWORD>(offset >> 32);
     DWORD cbRead;
     return ReadFile(hFile, pvBuffer, cbBuffer, &cbRead, &o) &&
            cbBuffer == cbRead;
    }
    

    We merge the Set­File­Pointer call into the Read­File by specifying the desired byte offset in the optional OVERLAPPED structure. The I/O will still complete synchronously (since we opened the handle synchronously), but we saved ourselves the hassle of having to call two functions when it could be done with just one.

  • The Old New Thing

    I know that an overlapped file handle requires an lpOverlapped, but why does it (sometimes) work if I omit it?

    • 27 Comments

    A customer observed that the formal requirements for the Read­File function specify that if the handle was opened with FILE_FLAG_OVERLAPPED, then the lpOverlapped parameter is mandatory. But the customer observed that in practice, passing NULL results in strange behavior. Sometimes the call succeeds, and sometimes it even returns (horrors!) valid data. (Actually the more horrifying case is where the call succeeds and returns bogus data!)

    Now sure, you violated one of the requirements for the function, so the behavior is undefined. But why doesn't Read­File just flat-out fail if you call it incorrectly?

    The answer is that the Read­File function doesn't know whether you're calling it correctly.

    The Read­File function doesn't know whether the handle you passed was opened for overlapped or synchronous access. It just trusts that you're calling it correctly and builds an asynchronous call to pass into the kernel. If you passed a synchronous handle, well, it just issues the I/O request into the kernel anyway, and you get what you get.

    This quirk traces its history all the way back to the Microsoft Windows NT OS/2 Design Workbook. As originally designed, Windows NT had a fully asynchronous kernel. There was no such thing as a blocking read. If you wanted a blocking read, you had to issue an asynchronous read (the only kind available), and then block on it.

    As it turns out, developers vastly prefer synchronous reads. Writing asynchronous code is hard. So the kernel folks relented and said, "Okay, we'll have a way for you to specify at creation time whether you want a handle to be synchronous or asynchronous. And since lazy people prefer synchronous I/O, we'll make synchronous I/O the default, so that lazy people can keep being lazy."

    The Read­File function is a wrapper around the underlying Nt­Read­File function. If you pass an lpOverlapped, then it takes the OVERLAPPED structure apart so it can pass the pieces as an Io­Status­Block and a Byte­Offset. (And if you don't pass an lpOverlapped, then it needs to create temporary buffers on the stack.) All this translation takes place without the Read­File function actually knowing whether the handle you passed is asynchronous or synchronous; that information isn't available to the Read­File function. It's relying on you, the caller, to pass the parameters correctly.

    As it happens, the Nt­Read­File function does detect that you are trying to perform synchronous I/O on an asynchronous handle and fails with STATUS_INVALID_PARAMETER (which the Read­File function turns into ERROR_INVALID_PARAMETER), so you know that something went wrong.

    Unless you are a pipe or mailslot.

    For some reason, if you attempt to issue synchronous I/O on an asynchronous handle to a pipe or mailslot, the I/O subsystem says, "Sure, whatever." I suspect this is somehow related to the confusing no-wait model for pipes.

    Long before this point, the basic ground rules for programming kicked in. "Pointers are not NULL unless explicitly permitted otherwise," and the documentation clearly forbids passing NULL for asynchronous handles. The behavior that results from passing invalid parameters is undefined, so you shouldn't be surprised that the results are erratic.

  • The Old New Thing

    There's the interface contract, and there are the implementations of the interface contract

    • 28 Comments

    Ivo wants to know whether it is legal to use NULL as the icon parameters to IExtractIcon::Extract. The documentation says that the parameters are optional, but some shell folder implementations treat them as mandatory.

    Yes, the parameters are technically optional, but it's also the case that many people mess up their implementation of the interface and treat them as mandatory, either by crashing on a null pointer or by returning E_INVALIDARG. Since IExtractIcon is an extension interface, you are pretty much at the mercy of all the implementations of that extension.

    Welcome to the land of application compatibility, where you have to incorporate workarounds for other people's bugs. In this case, it means always passing non-null pointers for the output icons if you want to get anything meaningful back, even if that means asking for more than you really need and then throwing part of it away.

    Ivo's second question was whether there is a performance benefit of asking just for the icon you want, or whether it's almost as fast to get both.

    If you ask for just one of the icons, then the icon extractor doesn't need to go extract it, which saves you a small amount of disk access (or a large amount if you're asking for the monster 256×256 icon). But given that compatibility forces you to ask for both anyway, the answer doesn't help you any. Given that there are drivers who run red lights, you could say that, in theory, "It is more efficient to cross the street as soon as the light turns green," but in practice, you'd be better served to look for traffic before stepping out into the roadway.

    You'd be right, but you'd be dead right.

Page 1 of 3 (22 items) 123