greateric's blog

By greateric, history, 3 months ago, In English

In practice our C++ solutions get to be run with Undefined Behavior Sanitizer, also known as the diagnostics, which catch things like bad memory accesses from out of bounds array indices and integer overflows.

I think we should be allowed to enable this during a submission during an actual contest. You'd have a checkbox at the bottom that says something like "enable diagnostic checks (warning: code may run much slower)" where you can choose to enable it or not.

Is this unfair? I don't think so, UBSan is basically just a souped up template and you can achieve the same effect with one. You would be able to get the same effect by jamming assert(result of addition did not overflow) before every addition or having a template safe int / safe long class that does that for you or __builtin_add_overflow. Of course, you shouldn't be told the line number or what the problem was, just runtime error should be sufficient. (Since if you do it yourself the only information you get is that a runtime error happened somewhere.)

Other languages also have some variant of this, like java has index out of bounds exception (though I don't believe they natively have overflow detection).

It's also somewhat limited in strength, since for your first submission you will probably leave it off (or else you will definitely TLE), so you will end up needing to spend another -50 penalty if you want to check if the problem was an overflow/UB or not.

Is this a good idea or is this just cope from failing round 1102 E

PS: While we currently don't have that, I found that while practicing it's helpful to use an adblocker to stop myself from accidentally seeing the "diagnostics hint" icon since it's a big giveaway to what the problem is. In uBlock Origin Lite you can do "create a custom filter" -> click on the triangle icon.

  • Vote: I like it
  • -17
  • Vote: I do not like it

»
3 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Auto comment: topic has been updated by greateric (previous revision, new revision, compare).

»
3 months ago, hide # |
 
Vote: I like it +15 Vote: I do not like it

It'd be a waste of effort on our side and on CF side both. This kind of run doesn't give you valuable information (keep in mind that your code can get Runtime Error without UB, you're basically told that you did something wrong... which you already knew) and you need to debug your code locally anyway. Guess what flags you can add when you debug your code locally? It's even more damning that due to performance drops from debug+sanitizer builds, it only applies for stresstests which are extra simple to run locally. Any added value just isn't there. Don't rely on the judging system to give you more than a basic fuzzy hint, rely on local testing.

  • »
    »
    3 months ago, hide # ^ |
     
    Vote: I like it -15 Vote: I do not like it

    TBH I don't get why everyone likes to do local stresstesting. It:

    • Takes time to write a brute forcer and checker
    • Some problems your brute forcer will be horrifically bad in complexity or a problem will just not be brute forceable at all
    • I'd rather tank 50 points and see "RE on test 4" and know I have something weird
    • Some edge cases are so infinitely unlikely to be caught during stresstesting. Best examples I can think of is 0 on the recent Cirno and Number or if you're recursing quicksort style and have a recursion tree thats usually n log n but becomes n^2 that only shows up against adversarial inputs.

    In either case, regardless of what might be better for most people, giving the option should never be a bad thing.

    • »
      »
      »
      3 months ago, hide # ^ |
       
      Vote: I like it 0 Vote: I do not like it

      You don't even understand what stresstesting is. It's not supposed to check if your solution is efficient. It's just running your code on a huge number of small tests. Catching edge cases you didn't think of is, in fact, an important role here.

      Adversarial inputs aren't usually edge cases either. They're either broad but not considered when generating tests (by nature of how they're made, larger used test data is a tiny subset of all possibilities so there's a wide space where adversarial tests could still be; a tiny subset can nonetheless catch pretty much all wrong solutions during a contest) or crafted to break a particular solution.

      Writing a brute force and generating data is easy if you even believe you can solve the problem. If writing a brute force is prohibitively difficult, you're better off skipping the problem because the authors must've struggled with it too and therefore it's too hard or likely broken, or you misunderstood the problem and need to stop asking for unnecessary tools and start rereading statements. Solving a problem efficiently is step 2. Solving that problem at all is step 1.

      If you need to write a checker and struggle with it, the problem must be sufficiently hard to check, therefore you're struggling to know if your solution's even correct. Checking things by hand is harder. Once you know there's something wrong with your code, you'll need to check your outputs somehow if only to find one wrong output. You can't fix your code if you can't see it do something you understand to be wrong.

      I start implementing interactive problems by writing a built-in interactor (both sides) and checker because it's a pain to keep track of queries and responses if I actually interact with it. Makes debugging easier too.

      "I'd rather tank 50 points and see "RE on test 4" and know I have something weird" is covered by "since for your first submission you will probably leave it off" — you're already doing it, everyone's already doing it, so you're not arguing about added value here.

      I'm gonna bet if this feature existed from the start, it wouldn't have given you useful info. You can see RE and make a wrong assumption that leads you to a bug, but you can make a wrong assumption even without it, it's complete placebo. If it gets used a lot without giving useful info, it's wasted CPU time and that's a bad thing. Even if this feature doesn't get used at all, it's wasted time implementing it and that's a bad thing.

»
3 months ago, hide # |
 
Vote: I like it +3 Vote: I do not like it

UBSan (or ASAN or other memory access checker) isn't useful on CF submissions, because you only get RE anyway in test 2 or later. It's indistinguishable whether your code fails with UBSan or your code really crashes. You can tell your code hits UBSan in test 1, but that can be done locally.

Just use them locally.

  • »
    »
    3 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    WA vs RE gives a lot of useful information in my opinion. When I see WA2 I think "I'm fucked", when I see RE2 I think "oh wait ok I tripped an assertion somewhere this is saveable"

    • »
      »
      »
      3 months ago, hide # ^ |
       
      Vote: I like it 0 Vote: I do not like it

      There are plenty of reasons why you can get RE other than assertions. You only trip assertions that are written into your code and those give you RE anyway.

      You should read RE as something wrong in your code, some logic error that makes things blow up in ways that show up as RE before they show up as WA. There's no fundamental difference. The same code can even get WA before it can get RE if internal stuff happens in the right way.

      • »
        »
        »
        »
        3 months ago, hide # ^ |
         
        Vote: I like it 0 Vote: I do not like it

        Whatever the cause, seeing WA (but no RE from assertion / stack overflow / etc) vs seeing RE (now you know it's an assertion or some kind of UB) gives you information.

        When I see the triangle from the diagnostics I pretty much instantly know I had an overflow bug, and WA vs RE is the same idea (albeit the information is weaker).

        • »
          »
          »
          »
          »
          3 months ago, hide # ^ |
           
          Vote: I like it 0 Vote: I do not like it

          You're approaching debugging in a fundamentally wrong way. You shouldn't be looking for "did the OS return a non-zero error code?" or "did the output of my program not match what was expected?" because those are only the visible effects of your mistake, only symptoms. You can use them as weak hints but if you overrely on them, it'll just make your performance worse.

          It's easy to design a code where the observable output changes depending on a combination of mistakes you make:

          ...
          std::vector<int> v(n+1);
          ...
          int s = 0;
          for(int i = 0; i <= n; i++) s += v[i];
          ...
          

          This gets WA if your goal is to sum up $$$n$$$ numbers. Changing the vector's size to n possibly changes WA to RE but the code is still wrong, almost equally wrong with unimportant difference. If you attach deeper meanings to verdicts, you'd assume that something meaningfully changed when RE changed to WA or vice versa, but here it's not the case. I've made that mistake before.

          It can be useful to run over a wrong code with a combo of sanitizers (I use clang's ASan+UBSan), fix some obvious bugs or focus on parts around the lines where it pointed something out. However, you need to see the actual sanitizer output to do that, so it's still local testing.

          Relying on one extra verdict (in addition to what we already have i.e. default + diagnostics) is more augury than programming.